<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ refactoring - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ refactoring - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Fri, 23 Oct 2026 06:34:17 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/refactoring/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Migrate a Legacy Monolith Incrementally Without a Big-Bang Rewrite ]]>
                </title>
                <description>
                    <![CDATA[ Large legacy migrations often fail long before the final cutover. The failure usually starts when the migration is framed as a single event. Move the application. Move the database. Move all the users ]]>
                </description>
                <link>https://www.freecodecamp.org/news/migrate-legacy-monolith-incrementally/</link>
                <guid isPermaLink="false">6aac7747d406d7c207351312</guid>
                
                    <category>
                        <![CDATA[ legacy code ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software architecture ]]>
                    </category>
                
                    <category>
                        <![CDATA[ migration ]]>
                    </category>
                
                    <category>
                        <![CDATA[ refactoring ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Artificial Intelligence ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Hugo Teijiz ]]>
                </dc:creator>
                <pubDate>Thu, 17 Sep 2026 23:27:03 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/086653d6-268e-4d13-8f7d-b42c92847361.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Large legacy migrations often fail long before the final cutover.</p>
<p>The failure usually starts when the migration is framed as a single event. Move the application. Move the database. Move all the users. Switch the traffic. Turn the old system off.</p>
<p>That creates a dangerous assumption: the legacy system and the new system need to exchange places all at once.</p>
<p>They usually don't.</p>
<p>If you already understand the legacy behavior, protect it with characterization tests, create migration-friendly boundaries, and compare old and new implementations, you have another option.</p>
<p>You can migrate one capability at a time. That changes the problem completely.</p>
<p>Instead of:</p>
<pre><code class="language-text">legacy monolith
      ↓
complete rewrite
      ↓
big-bang cutover
</code></pre>
<p>you can move toward:</p>
<pre><code class="language-text">legacy monolith
      ↓
one capability extracted
      ↓
small percentage of traffic
      ↓
observe
      ↓
expand
      ↓
repeat
</code></pre>
<p>The goal isn't to make the migration slower. The goal is to make each change smaller, observable, and reversible.</p>
<p>In this tutorial, I'll show you how to migrate a legacy monolith incrementally by:</p>
<ul>
<li><p>choosing a safe first migration slice</p>
</li>
<li><p>defining a boundary between legacy and new code</p>
</li>
<li><p>routing requests between implementations</p>
</li>
<li><p>using the Strangler Fig pattern</p>
</li>
<li><p>migrating by business capability instead of technical layer</p>
</li>
<li><p>keeping old and new implementations running together</p>
</li>
<li><p>introducing progressive traffic</p>
</li>
<li><p>detecting failures before full cutover</p>
</li>
<li><p>designing rollback paths</p>
</li>
<li><p>handling data ownership carefully</p>
</li>
<li><p>removing migrated legacy behavior</p>
</li>
<li><p>using AI without turning an incremental migration into an automated rewrite</p>
</li>
</ul>
<p>The examples use TypeScript, but the approach applies to most languages, runtimes, and architectures.</p>
<p>The objective is simple: make migration a sequence of controlled changes instead of one irreversible event.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along, you should be comfortable with:</p>
<ul>
<li><p>TypeScript or a similar language</p>
</li>
<li><p>API and service boundaries</p>
</li>
<li><p>integration testing</p>
</li>
<li><p>dependency injection</p>
</li>
<li><p>routing and reverse proxies</p>
</li>
<li><p>database transactions</p>
</li>
<li><p>observability</p>
</li>
<li><p>incremental refactoring</p>
</li>
<li><p>legacy modernization</p>
</li>
</ul>
<p>You should also already understand the behavior of the capability you want to migrate.</p>
<p>Ideally, you know:</p>
<ul>
<li><p>its inputs</p>
</li>
<li><p>its outputs</p>
</li>
<li><p>its important business rules</p>
</li>
<li><p>its side effects</p>
</li>
<li><p>its dependencies</p>
</li>
<li><p>its external contracts</p>
</li>
<li><p>how you'll detect behavioral differences</p>
</li>
</ul>
<p>If you haven't reached that point yet, migration may be premature.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-why-big-bang-migrations-are-so-risky">Why Big-Bang Migrations Are So Risky</a></p>
</li>
<li><p><a href="#heading-think-in-migration-slices-not-applications">Think in Migration Slices, Not Applications</a></p>
</li>
<li><p><a href="#heading-choose-the-first-capability-carefully">Choose the First Capability Carefully</a></p>
</li>
<li><p><a href="#heading-create-a-boundary-between-legacy-and-new">Create a Boundary Between Legacy and New</a></p>
</li>
<li><p><a href="#heading-use-the-strangler-fig-pattern">Use the Strangler Fig Pattern</a></p>
</li>
<li><p><a href="#heading-migrate-capabilities-not-technical-layers">Migrate Capabilities, Not Technical Layers</a></p>
</li>
<li><p><a href="#heading-keep-legacy-and-new-implementations-running-together">Keep Legacy and New Implementations Running Together</a></p>
</li>
<li><p><a href="#heading-route-traffic-explicitly">Route Traffic Explicitly</a></p>
</li>
<li><p><a href="#heading-start-with-internal-or-low-risk-traffic">Start with Internal or Low-Risk Traffic</a></p>
</li>
<li><p><a href="#heading-progressively-increase-production-traffic">Progressively Increase Production Traffic</a></p>
</li>
<li><p><a href="#heading-use-differential-testing-before-and-during-rollout">Use Differential Testing Before and During Rollout</a></p>
</li>
<li><p><a href="#heading-a-small-end-to-end-invoice-migration-example">A Small End-to-End Invoice Migration Example</a></p>
</li>
<li><p><a href="#heading-design-rollback-before-you-need-it">Design Rollback Before You Need It</a></p>
</li>
<li><p><a href="#heading-treat-data-migration-as-a-separate-problem">Treat Data Migration as a Separate Problem</a></p>
</li>
<li><p><a href="#heading-be-careful-with-dual-writes">Be Careful with Dual Writes</a></p>
</li>
<li><p><a href="#heading-decide-who-owns-the-data">Decide Who Owns the Data</a></p>
</li>
<li><p><a href="#heading-observe-business-behavior-not-just-infrastructure">Observe Business Behavior, Not Just Infrastructure</a></p>
</li>
<li><p><a href="#heading-know-when-a-migration-slice-is-complete">Know When a Migration Slice Is Complete</a></p>
</li>
<li><p><a href="#heading-remove-the-legacy-path">Remove the Legacy Path</a></p>
</li>
<li><p><a href="#heading-how-to-use-ai-during-an-incremental-migration">How to Use AI During an Incremental Migration</a></p>
</li>
<li><p><a href="#heading-do-not-let-ai-turn-the-migration-into-a-rewrite">Do Not Let AI Turn the Migration into a Rewrite</a></p>
</li>
<li><p><a href="#heading-a-practical-incremental-migration-workflow">A Practical Incremental Migration Workflow</a></p>
</li>
<li><p><a href="#heading-what-incremental-migration-does-not-solve">What Incremental Migration Does Not Solve</a></p>
</li>
<li><p><a href="#heading-the-complete-legacy-modernization-workflow">The Complete Legacy Modernization Workflow</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-why-big-bang-migrations-are-so-risky">Why Big-Bang Migrations Are So Risky</h2>
<p>Imagine a legacy commerce application.</p>
<p>It contains:</p>
<pre><code class="language-text">customers
orders
payments
inventory
shipping
invoicing
notifications
reporting
</code></pre>
<p>The modernization plan says:</p>
<pre><code class="language-text">replace the monolith
</code></pre>
<p>That sounds like one project.</p>
<p>Operationally, it may mean changing:</p>
<pre><code class="language-text">runtime
framework
database
deployment model
API contracts
authentication
networking
observability
data model
business logic
external integrations
</code></pre>
<p>at the same time.</p>
<p>If the final cutover fails, the number of possible causes is enormous.</p>
<p>For example:</p>
<pre><code class="language-text">Did pricing change?

Did the database migration lose data?

Is the payment provider failing?

Did authentication behave differently?

Did the new runtime change date handling?

Did a timeout become shorter?

Did an event stop being published?

Did the new deployment configuration fail?
</code></pre>
<p>This is one of the central problems with big-bang migration: too many variables change together.</p>
<p>Incremental migration tries to reduce the number of changing variables at each step.</p>
<h2 id="heading-think-in-migration-slices-not-applications">Think in Migration Slices, Not Applications</h2>
<p>Instead of asking:</p>
<blockquote>
<p>How do we migrate this monolith?</p>
</blockquote>
<p>ask:</p>
<blockquote>
<p>What's the smallest meaningful business capability we can move independently?</p>
</blockquote>
<p>For example:</p>
<pre><code class="language-text">Calculate Order Total
Generate Invoice
Send Order Confirmation
Create Shipment
Renew Subscription
Approve Customer
</code></pre>
<p>A migration slice should ideally have:</p>
<pre><code class="language-text">clear input
clear output
known side effects
understood dependencies
observable behavior
a rollback path
</code></pre>
<p>That gives you something concrete to move.</p>
<p>For example:</p>
<pre><code class="language-text">Generate Invoice
</code></pre>
<p>might become:</p>
<pre><code class="language-text">input:
orderId

behavior:
load order
calculate taxes
generate invoice number
create invoice

side effects:
store invoice
publish invoice.created

output:
invoice
</code></pre>
<p>That's much easier to migrate than:</p>
<pre><code class="language-text">billing module
</code></pre>
<p>or:</p>
<pre><code class="language-text">src/services/
</code></pre>
<p>Business capabilities make better migration units than folders.</p>
<h2 id="heading-choose-the-first-capability-carefully">Choose the First Capability Carefully</h2>
<p>The first slice matters.</p>
<p>I would usually avoid starting with the most critical capability in the system.</p>
<p>You want something meaningful enough to validate the migration approach, but not so dangerous that a mistake creates catastrophic consequences.</p>
<p>A useful first slice often has:</p>
<pre><code class="language-text">moderate traffic
limited external dependencies
clear behavior
good test coverage
few transactional boundaries
low blast radius
</code></pre>
<p>For example:</p>
<pre><code class="language-text">Generate Customer Statement
</code></pre>
<p>may be a better first migration candidate than:</p>
<pre><code class="language-text">Authorize Payment
</code></pre>
<p>The first migration is partly technical work, but it's also a learning exercise.</p>
<p>You're validating:</p>
<pre><code class="language-text">routing
deployment
observability
rollback
data access
testing
team workflow
</code></pre>
<p>before applying the pattern to more critical capabilities.</p>
<h2 id="heading-create-a-boundary-between-legacy-and-new">Create a Boundary Between Legacy and New</h2>
<p>Suppose the legacy application has:</p>
<pre><code class="language-typescript">async function generateInvoice(
  orderId: string
) {
  // legacy implementation
}
</code></pre>
<p>Before migration, introduce a boundary:</p>
<pre><code class="language-typescript">interface InvoiceGenerator {
  generate(
    orderId: string
  ): Promise&lt;Invoice&gt;;
}
</code></pre>
<p>The legacy implementation becomes:</p>
<pre><code class="language-typescript">class LegacyInvoiceGenerator
  implements InvoiceGenerator {
  async generate(
    orderId: string
  ): Promise&lt;Invoice&gt; {
    // existing behavior
  }
}
</code></pre>
<p>The new implementation becomes:</p>
<pre><code class="language-typescript">class NewInvoiceGenerator
  implements InvoiceGenerator {
  async generate(
    orderId: string
  ): Promise&lt;Invoice&gt; {
    // migrated behavior
  }
}
</code></pre>
<p>Now the caller doesn't need to know which implementation is active.</p>
<p>That creates an important capability:</p>
<pre><code class="language-text">replace implementation
without replacing caller
</code></pre>
<p>which is one of the foundations of incremental migration.</p>
<h2 id="heading-use-the-strangler-fig-pattern">Use the Strangler Fig Pattern</h2>
<p>A common way to describe incremental replacement is the Strangler Fig pattern.</p>
<p>Instead of replacing the entire application at once, new behavior gradually grows around the old system.</p>
<p>Conceptually:</p>
<pre><code class="language-text">            incoming request
                   │
                   ↓
                router
              /        \
             /          \
      legacy path     new path
</code></pre>
<p>At first:</p>
<pre><code class="language-text">legacy: 100%
new:      0%
</code></pre>
<p>Later:</p>
<pre><code class="language-text">legacy: 95%
new:      5%
</code></pre>
<p>Then:</p>
<pre><code class="language-text">legacy: 50%
new:     50%
</code></pre>
<p>Eventually:</p>
<pre><code class="language-text">legacy:  0%
new:    100%
</code></pre>
<p>At that point, the old implementation for that capability can be removed.</p>
<p>The key is that the replacement happens gradually. The legacy application continues serving parts of the system while the new implementation takes over others.</p>
<h2 id="heading-migrate-capabilities-not-technical-layers">Migrate Capabilities, Not Technical Layers</h2>
<p>One tempting migration strategy is:</p>
<pre><code class="language-text">move database
then move services
then move APIs
then move UI
</code></pre>
<p>That can create long periods where every capability spans both old and new architecture.</p>
<p>For example:</p>
<pre><code class="language-text">new API
↓
legacy service
↓
new database
↓
legacy event publisher
</code></pre>
<p>This is sometimes unavoidable.</p>
<p>But whenever possible, I prefer vertical slices.</p>
<p>A vertical slice might be:</p>
<pre><code class="language-text">Generate Invoice

request
↓
application logic
↓
persistence
↓
events
↓
response
</code></pre>
<p>That capability can move as one coherent unit.</p>
<p>Then:</p>
<pre><code class="language-text">Create Shipment
</code></pre>
<p>can move separately.</p>
<p>Then:</p>
<pre><code class="language-text">Renew Subscription
</code></pre>
<p>and so on.</p>
<p>This gives you working migrated capabilities earlier. It also reduces the number of temporary cross-system dependencies.</p>
<h2 id="heading-keep-legacy-and-new-implementations-running-together">Keep Legacy and New Implementations Running Together</h2>
<p>During an incremental migration, coexistence is normal.</p>
<p>For some period of time, you may have:</p>
<pre><code class="language-text">LegacyInvoiceGenerator
NewInvoiceGenerator
</code></pre>
<p>both deployed.</p>
<p>That's not duplication by accident. It's part of the migration strategy.</p>
<p>The important question is how requests choose between them.</p>
<p>You may use:</p>
<pre><code class="language-text">feature flag
tenant
user group
request header
region
percentage rollout
specific account IDs
</code></pre>
<p>For example:</p>
<pre><code class="language-typescript">class InvoiceRouter {
  constructor(
    private readonly legacy:
      InvoiceGenerator,
    private readonly migrated:
      InvoiceGenerator
  ) {}

  async generate(
    orderId: string,
    useMigrated: boolean
  ) {
    if (useMigrated) {
      return this.migrated.generate(
        orderId
      );
    }

    return this.legacy.generate(
      orderId
    );
  }
}
</code></pre>
<p>This is deliberately simple. The important part is that routing is explicit. You know which implementation handled each request.</p>
<h2 id="heading-route-traffic-explicitly">Route Traffic Explicitly</h2>
<p>Avoid migration logic that's difficult to observe.</p>
<p>For example:</p>
<pre><code class="language-typescript">try {
  return await newService.call();
} catch {
  return legacyService.call();
}
</code></pre>
<p>This may look resilient, but it can hide failures.</p>
<p>Suppose the new implementation fails 40% of the time. If every failure silently falls back to legacy, users may see no problem. But the migration isn't healthy.</p>
<p>The problem is that the first version mixes two decisions together: <strong>which implementation should receive the request</strong> and <strong>what should happen when that implementation fails</strong>. Because the fallback happens inside the <code>catch</code>, the migrated path can fail repeatedly without producing an explicit routing signal that you can measure.</p>
<p>A better approach is to make the routing decision first, record it, and then call the selected implementation. That separates migration policy from error handling and gives you a clear record of how much traffic actually reached each path.</p>
<p>For example:</p>
<pre><code class="language-typescript">const route =
  migrationPolicy.route(request);

metrics.increment(
  `invoice.route.${route}`
);

if (route === "migrated") {
  return migrated.generate(
    request.orderId
  );
}

return legacy.generate(
  request.orderId
);
</code></pre>
<p>Now you can measure:</p>
<pre><code class="language-text">requests routed to legacy
requests routed to migrated
migration failures
fallback count
latency
business outcomes
</code></pre>
<p>Migration should be observable as a first-class system behavior.</p>
<h2 id="heading-start-with-internal-or-low-risk-traffic">Start with Internal or Low-Risk Traffic</h2>
<p>Before routing a large percentage of customers to the migrated path, start with safer traffic.</p>
<p>For example:</p>
<pre><code class="language-text">development
test environments
internal users
staff accounts
test tenants
specific low-risk customers
</code></pre>
<p>This lets you validate:</p>
<pre><code class="language-text">deployment
routing
observability
data access
external integrations
failure handling
</code></pre>
<p>with lower risk.</p>
<p>You can then expand.</p>
<p>For example:</p>
<pre><code class="language-text">internal users
↓
1% production
↓
5%
↓
10%
↓
25%
↓
50%
↓
100%
</code></pre>
<p>The exact percentages aren't important, but the principle is.</p>
<p>Each increase should happen because the previous stage produced enough evidence.</p>
<h2 id="heading-progressively-increase-production-traffic">Progressively Increase Production Traffic</h2>
<p>Suppose you have:</p>
<pre><code class="language-text">10,000 invoice requests/day
</code></pre>
<p>Instead of switching all requests:</p>
<pre><code class="language-text">legacy → new
</code></pre>
<p>at once, route:</p>
<pre><code class="language-text">1%
</code></pre>
<p>first.</p>
<p>That gives roughly:</p>
<pre><code class="language-text">100 real requests/day
</code></pre>
<p>through the migrated path.</p>
<p>Now monitor:</p>
<pre><code class="language-text">error rate
latency
output differences
side effects
customer-visible failures
business metrics
</code></pre>
<p>If the system behaves correctly, increase traffic. If it doesn't, reduce or disable migrated routing.</p>
<p>The migration becomes a controlled experiment. That's very different from a cutover event.</p>
<h2 id="heading-use-differential-testing-before-and-during-rollout">Use Differential Testing Before and During Rollout</h2>
<p>The <a href="https://www.freecodecamp.org/news/differential-testing-legacy-migration/">previous article in this series focused on differential testing</a>. That technique becomes especially useful here.</p>
<p>Differential testing means running the legacy and migrated implementations with the same input and comparing their observable behavior. Depending on the capability, that may include return values, errors, state changes, and side effects.</p>
<p>The goal isn't to prove that the implementations are internally identical. It's to detect meaningful behavioral differences before those differences reach all of your production traffic.</p>
<p>Before live routing, you can compare:</p>
<pre><code class="language-text">same input
↓
legacy result

same input
↓
new result
</code></pre>
<p>During rollout, you can also sample real traffic and compare behavior where it is safe to do so.</p>
<p>For example:</p>
<pre><code class="language-text">real request
      │
      ├────→ active implementation
      │
      └────→ shadow implementation
</code></pre>
<p>Then compare:</p>
<pre><code class="language-text">output
errors
side effects
business state
</code></pre>
<p>This gives you evidence before increasing traffic.</p>
<p>A rollout decision can then be based on:</p>
<pre><code class="language-text">divergence
error rate
latency
business outcomes
</code></pre>
<p>instead of:</p>
<blockquote>
<p>It seems fine.</p>
</blockquote>
<h2 id="heading-a-small-end-to-end-invoice-migration-example">A Small End-to-End Invoice Migration Example</h2>
<p>The individual pieces are easier to understand when you see them working together.</p>
<p>Here's a deliberately small, in-memory example based on the invoice capability we've been using throughout the article. It doesn't include a real database, reverse proxy, queue, or deployment platform. The point is to show the migration control flow in one place.</p>
<p>Start with a shared contract:</p>
<pre><code class="language-typescript">type InvoiceInput = {
  orderId: string;
  subtotal: number;
};

type Invoice = {
  orderId: string;
  total: number;
};

interface InvoiceGenerator {
  generate(
    input: InvoiceInput
  ): Promise&lt;Invoice&gt;;
}
</code></pre>
<p>The legacy implementation calculates the invoice total like this:</p>
<pre><code class="language-typescript">class LegacyInvoiceGenerator
  implements InvoiceGenerator {
  async generate(
    input: InvoiceInput
  ): Promise&lt;Invoice&gt; {
    return {
      orderId: input.orderId,
      total: input.subtotal * 1.21,
    };
  }
}
</code></pre>
<p>Now imagine we've migrated that capability into a new implementation:</p>
<pre><code class="language-typescript">class MigratedInvoiceGenerator
  implements InvoiceGenerator {
  async generate(
    input: InvoiceInput
  ): Promise&lt;Invoice&gt; {
    const tax =
      input.subtotal * 0.21;

    return {
      orderId: input.orderId,
      total: input.subtotal + tax,
    };
  }
}
</code></pre>
<p>The code is different, but the intended behavior is the same.</p>
<p>Next, define a deterministic rollout function. This example assigns each <code>orderId</code> to a bucket from 0 to 99 so the same order always follows the same route:</p>
<pre><code class="language-typescript">function bucketFor(
  value: string
): number {
  const sum = [...value].reduce(
    (total, char) =&gt;
      total + char.charCodeAt(0),
    0
  );

  return sum % 100;
}

function shouldUseMigrated(
  orderId: string,
  percentage: number
): boolean {
  return (
    bucketFor(orderId) &lt; percentage
  );
}
</code></pre>
<p>If <code>percentage</code> is <code>10</code>, roughly 10% of IDs will be assigned to the migrated path.</p>
<p>Now add some tiny in-memory metrics:</p>
<pre><code class="language-typescript">const metrics = {
  legacyRequests: 0,
  migratedRequests: 0,
  mismatches: 0,
};
</code></pre>
<p>Then put the legacy and migrated implementations behind one migration-aware entry point:</p>
<pre><code class="language-typescript">class IncrementalInvoiceService {
  migratedEnabled = true;
  rolloutPercentage = 10;

  constructor(
    private readonly legacy:
      InvoiceGenerator,
    private readonly migrated:
      InvoiceGenerator
  ) {}

  async generate(
    input: InvoiceInput
  ): Promise&lt;Invoice&gt; {
    const legacyResult =
      await this.legacy.generate(
        structuredClone(input)
      );

    const migratedResult =
      await this.migrated.generate(
        structuredClone(input)
      );

    if (
      migratedResult.orderId !==
        legacyResult.orderId ||
      migratedResult.total !==
        legacyResult.total
    ) {
      metrics.mismatches += 1;
    }

    const useMigrated =
      this.migratedEnabled &amp;&amp;
      shouldUseMigrated(
        input.orderId,
        this.rolloutPercentage
      );

    if (useMigrated) {
      metrics.migratedRequests += 1;
      return migratedResult;
    }

    metrics.legacyRequests += 1;
    return legacyResult;
  }
}
</code></pre>
<p>This small service combines several ideas from the article.</p>
<p>First, it runs both implementations with the same input and compares their results. Because this example is entirely in memory and has no external side effects, doing that is safe.</p>
<p>Second, it routes only a percentage of requests to the migrated result.</p>
<p>Third, it records how many requests used each path and how many behavioral mismatches occurred.</p>
<p>You can exercise it with a few requests:</p>
<pre><code class="language-typescript">const service =
  new IncrementalInvoiceService(
    new LegacyInvoiceGenerator(),
    new MigratedInvoiceGenerator()
  );

for (let i = 1; i &lt;= 100; i++) {
  await service.generate({
    orderId: `order-${i}`,
    subtotal: 1000,
  });
}

console.log(metrics);
</code></pre>
<p>You might see something like:</p>
<pre><code class="language-text">legacyRequests:   89
migratedRequests: 11
mismatches:        0
</code></pre>
<p>The exact split may not be exactly 90/10 with only 100 inputs because the bucket function is intentionally simple. The important point is that routing is deterministic, measurable, and controlled by <code>rolloutPercentage</code>.</p>
<p>If the migrated implementation starts producing differences, the mismatch counter gives you an observable signal.</p>
<p>And if you decide the rollout should stop, rollback is explicit:</p>
<pre><code class="language-typescript">service.migratedEnabled = false;
</code></pre>
<p>From that point forward, all returned responses come from the legacy implementation again.</p>
<p>This is intentionally a simplified example. A production system would need stronger routing, real metrics, error handling, persistent state, and careful treatment of side effects.</p>
<p>In particular, you shouldn't blindly execute both implementations if generating an invoice sends email, writes to two production databases, charges a customer, or publishes externally visible events. In those cases, the shadow path needs recording adapters, isolated infrastructure, or another mechanism that lets you compare behavior without duplicating real effects.</p>
<p>But the control loop is the same:</p>
<pre><code class="language-text">same input
↓
compare legacy and migrated behavior
↓
route a small percentage
↓
observe
↓
expand or roll back
</code></pre>
<p>That is incremental migration in its smallest useful form.</p>
<h2 id="heading-design-rollback-before-you-need-it">Design Rollback Before You Need It</h2>
<p>Rollback shouldn't be invented during an incident. Before moving traffic, ask what happens if the migrated path fails.</p>
<p>For routing-level migrations, rollback may be simple:</p>
<pre><code class="language-text">migration flag = false
</code></pre>
<p>and traffic returns to:</p>
<pre><code class="language-text">legacy implementation
</code></pre>
<p>For example:</p>
<pre><code class="language-typescript">if (
  featureFlags.useNewInvoices
) {
  return migrated.generate(
    orderId
  );
}

return legacy.generate(orderId);
</code></pre>
<p>If the migrated path behaves incorrectly:</p>
<pre><code class="language-text">useNewInvoices = false
</code></pre>
<p>Rollback is almost immediate.</p>
<p>But rollback becomes more complicated when:</p>
<pre><code class="language-text">data format changes
new data is written
events differ
external systems are updated
legacy code cannot read new records
</code></pre>
<p>In those cases, rollback may require more than flipping a feature flag. You might need backward-compatible schemas so both versions can read the same records, compensating actions for external side effects, replayable events, reconciliation jobs, or a short period where the legacy system remains able to consume data written by the new path.</p>
<p>For higher-risk migrations, it can also help to define a rollback boundary in advance. For example: traffic can return to legacy until a new schema version is written, or after a particular external event is emitted, recovery requires compensation instead of a simple rollback. The important part is knowing when rollback is still reversible and when you've crossed into a different recovery strategy.</p>
<p>That's why rollback design needs to happen before deployment.</p>
<h2 id="heading-treat-data-migration-as-a-separate-problem">Treat Data Migration as a Separate Problem</h2>
<p>Application migration and data migration are related, but they aren't the same problem.</p>
<p>Suppose the legacy system stores:</p>
<pre><code class="language-json">{
  "customer_type": "P",
  "status": 2
}
</code></pre>
<p>while the new system stores:</p>
<pre><code class="language-json">{
  "customerType": "PREMIUM",
  "status": "APPROVED"
}
</code></pre>
<p>You now need to answer:</p>
<pre><code class="language-text">Which database is authoritative?

Can both systems read the same data?

Do we transform on read?

Do we migrate records in batches?

Do we replicate changes?

When does ownership change?
</code></pre>
<p>These decisions should be explicit. Otherwise the application migration may appear successful while the data boundary remains ambiguous.</p>
<h2 id="heading-be-careful-with-dual-writes">Be Careful with Dual Writes</h2>
<p>One common transition strategy is:</p>
<pre><code class="language-text">write to legacy database
+
write to new database
</code></pre>
<p>This is called dual writing, and it looks simple.</p>
<p>For example:</p>
<pre><code class="language-typescript">await legacyOrders.save(order);
await newOrders.save(order);
</code></pre>
<p>But what happens if:</p>
<pre><code class="language-text">legacy write succeeds
new write fails
</code></pre>
<p>Now the two systems disagree.</p>
<p>Or:</p>
<pre><code class="language-text">legacy write fails
new write succeeds
</code></pre>
<p>Same problem.</p>
<p>Dual writes create a distributed consistency problem.</p>
<p>If you use them, you need to think about:</p>
<pre><code class="language-text">retries
idempotency
reconciliation
ordering
partial failure
monitoring
</code></pre>
<p>Sometimes a safer approach is:</p>
<pre><code class="language-text">single authoritative write
↓
change event
↓
replication
</code></pre>
<p>or a transactional outbox.</p>
<p>There's no universal solution. The important point is not to treat dual writing as a trivial migration technique.</p>
<h2 id="heading-decide-who-owns-the-data">Decide Who Owns the Data</h2>
<p>During coexistence, data ownership can become confusing.</p>
<p>Imagine:</p>
<pre><code class="language-text">legacy system writes customers

new system writes invoices

both systems read orders
</code></pre>
<p>That may be perfectly reasonable, but it should be documented.</p>
<p>For each migrated capability, define:</p>
<pre><code class="language-text">system of record
write owner
readers
replication direction
consistency expectations
</code></pre>
<p>For example:</p>
<pre><code class="language-text">Invoices

Write owner:
new system

Source of truth:
new database

Legacy access:
read-only adapter

Replication:
new → legacy reporting store
</code></pre>
<p>Now the architecture has an explicit direction.</p>
<p>Without ownership rules, migrations often create permanent synchronization problems.</p>
<h2 id="heading-observe-business-behavior-not-just-infrastructure">Observe Business Behavior, Not Just Infrastructure</h2>
<p>During rollout, teams often monitor:</p>
<pre><code class="language-text">CPU
memory
latency
HTTP 500s
database connections
</code></pre>
<p>Those are important. But they're not enough.</p>
<p>Suppose:</p>
<pre><code class="language-text">HTTP 200 rate = 99.99%
</code></pre>
<p>while:</p>
<pre><code class="language-text">invoice totals are wrong
</code></pre>
<p>Infrastructure monitoring says:</p>
<pre><code class="language-text">healthy
</code></pre>
<p>But the business system is not healthy.</p>
<p>Migration observability should include domain signals.</p>
<p>For example:</p>
<pre><code class="language-text">orders processed
payments authorized
invoices generated
discount distribution
failed renewals
average invoice total
events published
</code></pre>
<p>If you know normal business behavior, unusual changes can expose migration defects that technical metrics miss.</p>
<h2 id="heading-know-when-a-migration-slice-is-complete">Know When a Migration Slice Is Complete</h2>
<p>A capability isn't fully migrated just because traffic reached 100%.</p>
<p>Before declaring it complete, I would verify:</p>
<pre><code class="language-text">100% traffic on new path
acceptable error rate
acceptable latency
behavioral differences resolved
side effects verified
data ownership established
rollback window completed
legacy callers removed
legacy writes stopped
observability in place
</code></pre>
<p>Then ask:</p>
<blockquote>
<p>Is the legacy implementation still serving any purpose?</p>
</blockquote>
<p>If not, remove it.</p>
<p>Leaving both implementations permanently active creates:</p>
<pre><code class="language-text">maintenance cost
confusion
duplicate bugs
unclear ownership
future migration debt
</code></pre>
<p>Incremental migration should eventually simplify the system, not permanently duplicate it.</p>
<h2 id="heading-remove-the-legacy-path">Remove the Legacy Path</h2>
<p>This step is often delayed.</p>
<p>Teams migrate traffic but leave the old path in place:</p>
<pre><code class="language-text">just in case
</code></pre>
<p>Months later:</p>
<pre><code class="language-text">nobody knows whether it is still used
</code></pre>
<p>Before deleting it, verify:</p>
<pre><code class="language-text">routing metrics show zero traffic
no callers depend on it
data dependencies are removed
rollback period is complete
operational documentation is updated
</code></pre>
<p>Then remove:</p>
<pre><code class="language-text">legacy implementation
legacy feature flags
legacy database access
unused integration code
temporary compatibility layers
</code></pre>
<p>Deletion is part of migration.</p>
<p>A migration that only adds new architecture without removing old architecture can increase complexity rather than reduce it.</p>
<h2 id="heading-how-to-use-ai-during-an-incremental-migration">How to Use AI During an Incremental Migration</h2>
<p>AI can help with many parts of this process.</p>
<p>For example, it can inspect the legacy codebase and help answer:</p>
<pre><code class="language-text">Which modules implement this capability?

Which callers depend on it?

Which database tables does it touch?

Which external services does it call?

Which side effects occur?

Which feature flags already exist?

Which paths need adapters?
</code></pre>
<p>A useful prompt might be:</p>
<pre><code class="language-text">Analyze the Generate Invoice capability.

Identify:

1. entry points,
2. business rules,
3. persistence dependencies,
4. external integrations,
5. side effects,
6. callers,
7. data ownership,
8. possible migration seams.

Do not redesign the system.

Return evidence for each finding using file paths
and relevant code references.
</code></pre>
<p>AI can also help compare migration changes.</p>
<p>For example:</p>
<pre><code class="language-text">Compare the legacy and migrated implementations.

Identify possible behavioral differences in:

- return values,
- errors,
- side effects,
- persistence,
- event ordering,
- retries,
- idempotency,
- transaction boundaries.

Do not assume the new implementation is correct.
</code></pre>
<p>This is useful because migration involves a lot of repetitive analysis, and AI can accelerate that analysis.</p>
<h3 id="heading-dont-let-ai-turn-the-migration-into-a-rewrite">Don't Let AI Turn the Migration into a Rewrite</h3>
<p>There's a common failure mode.</p>
<p>You ask:</p>
<blockquote>
<p>Help me migrate this legacy capability.</p>
</blockquote>
<p>The model responds with:</p>
<pre><code class="language-text">new architecture
new domain model
new API
new event model
new database schema
new validation layer
new framework
</code></pre>
<p>At that point, you're no longer migrating one capability, you're redesigning it.</p>
<p>Sometimes redesign is necessary, but it should be intentional.</p>
<p>During incremental migration, I prefer prompts with explicit constraints.</p>
<p>For example:</p>
<pre><code class="language-text">Migrate this capability without intentionally changing
observable behavior.

Preserve:

- inputs,
- outputs,
- errors,
- side effects,
- ordering where relevant,
- transactional behavior.

Only introduce the minimum structural changes required
to run it in the target environment.

List any behavior you cannot preserve with confidence.
</code></pre>
<p>That keeps the transformation narrow.</p>
<p>AI should help reduce mechanical effort. It shouldn't silently expand project scope.</p>
<h2 id="heading-a-practical-incremental-migration-workflow">A Practical Incremental Migration Workflow</h2>
<p>Here's the workflow I would use.</p>
<h3 id="heading-1-understand-the-capability">1. Understand the Capability</h3>
<p>Identify:</p>
<pre><code class="language-text">inputs
outputs
rules
side effects
dependencies
unknowns
</code></pre>
<h3 id="heading-2-characterize-existing-behavior">2. Characterize Existing Behavior</h3>
<p>Protect important behavior with:</p>
<pre><code class="language-text">characterization tests
integration tests
contract tests
</code></pre>
<h3 id="heading-3-refactor-for-migration">3. Refactor for Migration</h3>
<p>Create:</p>
<pre><code class="language-text">seams
adapters
explicit dependencies
clear orchestration
</code></pre>
<p>without intentionally changing behavior.</p>
<h3 id="heading-4-build-the-new-implementation">4. Build the New Implementation</h3>
<p>Implement the capability in the target environment. Keep its observable contract clear.</p>
<h3 id="heading-5-differentially-test-old-and-new">5. Differentially Test Old and New</h3>
<p>Compare:</p>
<pre><code class="language-text">outputs
errors
side effects
business state
</code></pre>
<p>using representative cases.</p>
<h3 id="heading-6-introduce-explicit-routing">6. Introduce Explicit Routing</h3>
<p>Allow requests to choose:</p>
<pre><code class="language-text">legacy
or
migrated
</code></pre>
<p>through an observable migration policy.</p>
<h3 id="heading-7-start-with-safe-traffic">7. Start with Safe Traffic</h3>
<p>Use:</p>
<pre><code class="language-text">internal users
test tenants
selected customers
</code></pre>
<h3 id="heading-8-increase-traffic-gradually">8. Increase Traffic Gradually</h3>
<p>For example:</p>
<pre><code class="language-text">1%
5%
10%
25%
50%
100%
</code></pre>
<p>only when evidence supports the next stage.</p>
<h3 id="heading-9-monitor-technical-and-business-metrics">9. Monitor Technical and Business Metrics</h3>
<p>Observe both:</p>
<pre><code class="language-text">system health
business behavior
</code></pre>
<h3 id="heading-10-keep-rollback-available">10. Keep Rollback Available</h3>
<p>Make returning to the legacy path fast and understood.</p>
<h3 id="heading-11-transfer-data-ownership">11. Transfer Data Ownership</h3>
<p>Explicitly define which system owns:</p>
<pre><code class="language-text">writes
reads
replication
</code></pre>
<h3 id="heading-12-remove-the-legacy-path">12. Remove the Legacy Path</h3>
<p>After the migration has stabilized:</p>
<pre><code class="language-text">delete old implementation
remove temporary routing
remove obsolete dependencies
</code></pre>
<p>Then choose the next capability.</p>
<h2 id="heading-what-incremental-migration-doesnt-solve">What Incremental Migration Doesn't Solve</h2>
<p>Incremental migration reduces risk, but it doesn't eliminate complexity.</p>
<p>You may still need to deal with:</p>
<pre><code class="language-text">distributed transactions
shared databases
old schemas
tight coupling
unsupported runtimes
poor test coverage
organizational ownership
regulatory constraints
</code></pre>
<p>There are also systems where partial migration is extremely difficult.</p>
<p>For example:</p>
<pre><code class="language-text">highly stateful systems
strongly coupled desktop applications
large transactional batch systems
systems with shared global state
</code></pre>
<p>Sometimes the migration boundary needs to be larger.</p>
<p>The principle remains the same:</p>
<blockquote>
<p>Make the smallest reversible change that produces useful migration progress.</p>
</blockquote>
<p>Incremental doesn't always mean tiny. It means controlled.</p>
<h2 id="heading-the-complete-legacy-modernization-workflow">The Complete Legacy Modernization Workflow</h2>
<p>This article closes the workflow we've been building throughout this series.</p>
<p>We started with a basic problem:</p>
<blockquote>
<p>How do you modernize a legacy application without accidentally turning the project into a rewrite?</p>
</blockquote>
<p>The first step was understanding.</p>
<pre><code class="language-text">Legacy system
↓
investigate
↓
map behavior and dependencies
</code></pre>
<p>Then characterization.</p>
<pre><code class="language-text">observed behavior
↓
tests
↓
behavioral safety net
</code></pre>
<p>Then refactoring.</p>
<pre><code class="language-text">entangled capability
↓
seams and boundaries
↓
migration-friendly structure
</code></pre>
<p>Then differential testing.</p>
<pre><code class="language-text">legacy implementation
        +
new implementation
        ↓
behavior comparison
</code></pre>
<p>And finally incremental migration.</p>
<pre><code class="language-text">Understand
↓
Characterize
↓
Refactor
↓
Migrate
↓
Compare
↓
Route
↓
Observe
↓
Expand
↓
Remove legacy
</code></pre>
<p>The sequence matters.</p>
<p>If you skip understanding, you may migrate the wrong behavior.</p>
<p>If you skip characterization, you may not notice behavioral changes.</p>
<p>If you skip refactoring, the migration boundary may remain too large.</p>
<p>If you skip comparison, differences remain hidden.</p>
<p>If you skip incremental rollout, you discover problems at full blast radius.</p>
<p>Each step reduces a different kind of uncertainty.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Modernizing a legacy application doesn't require replacing everything at once.</p>
<p>In many cases, the safer strategy is to create a path where old and new implementations can coexist temporarily.</p>
<p>Move one capability, then compare it.</p>
<p>Route a small amount of traffic and observe what happens.</p>
<p>Increase traffic when the evidence supports it, and roll back when it doesn't.</p>
<p>Transfer ownership explicitly, then remove the legacy path.</p>
<p>And repeat.</p>
<p>The full workflow becomes:</p>
<pre><code class="language-text">Understand
↓
Characterize
↓
Refactor
↓
Migrate incrementally
↓
Compare behavior
↓
Progressively route traffic
↓
Observe
↓
Remove legacy
</code></pre>
<p>AI can make every stage faster.</p>
<p>It can help map code, identify dependencies, generate adapters, compare implementations, analyze failures, and inspect migration diffs.</p>
<p>But speed isn't the same as confidence.</p>
<p>The important decisions still require engineering judgment:</p>
<pre><code class="language-text">What behavior matters?

What can change?

What should remain compatible?

What is the migration boundary?

What evidence is enough?

When is rollback necessary?

When can the legacy path be removed?
</code></pre>
<p>Those aren't code-generation questions. They're migration decisions.</p>
<p>And that's the larger lesson behind this entire series.</p>
<p>AI makes it increasingly cheap to produce new code. But that doesn't make legacy modernization trivial. It makes the quality of the decisions around the code more important.</p>
<p>Because the safest migration is rarely the one that changes the most software. It's the one that lets you change the system while continuously knowing what changed, why it changed, and whether it's safe to keep going.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Refactor a Legacy Application Before Migrating It ]]>
                </title>
                <description>
                    <![CDATA[ The moment a team decides to migrate a legacy application, there's usually pressure to start moving code. Move the database, the API, or the UI. Move the application to a new framework, runtime, cloud ]]>
                </description>
                <link>https://www.freecodecamp.org/news/refactor-legacy-application-before-migration/</link>
                <guid isPermaLink="false">6a9f3503813f6309fdfcd6e3</guid>
                
                    <category>
                        <![CDATA[ legacy code ]]>
                    </category>
                
                    <category>
                        <![CDATA[ refactoring ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software architecture ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Software Engineering ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Artificial Intelligence ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Hugo Teijiz ]]>
                </dc:creator>
                <pubDate>Mon, 07 Sep 2026 22:04:51 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/d9d0b4b3-b9f4-4f86-98ba-ec079ac68284.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>The moment a team decides to migrate a legacy application, there's usually pressure to start moving code.</p>
<p>Move the database, the API, or the UI. Move the application to a new framework, runtime, cloud provider, or architecture.</p>
<p>That sounds reasonable, but there's a problem.</p>
<p>If the current system mixes business rules, persistence, infrastructure, external integrations, and orchestration inside the same modules, migration becomes much harder than it needs to be.</p>
<p>You're not just moving software. You're trying to move several responsibilities that have become entangled over years of development.</p>
<p>This is why I often prefer to refactor <strong>before</strong> migrating. Not to make the legacy system beautiful or redesign everything. And definitely not to turn the preparation phase into another rewrite.</p>
<p>The goal is much narrower: change the structure enough that important behavior can move independently.</p>
<p>In the <a href="https://www.freecodecamp.org/news/modernize-legacy-applications-with-ai/">previous</a> <a href="https://www.freecodecamp.org/news/understand-a-legacy-codebase-with-ai/">steps</a> of this workflow, we first tried to understand the codebase and then used characterization tests to protect the behavior we were about to change.</p>
<p>Now you'll learn how you can start changing the structure.</p>
<p>In this tutorial, I'll show you how to prepare a legacy application for migration by:</p>
<ul>
<li><p>choosing a migration boundary,</p>
</li>
<li><p>separating business rules from infrastructure,</p>
</li>
<li><p>introducing seams,</p>
</li>
<li><p>isolating side effects,</p>
</li>
<li><p>creating adapters around external systems,</p>
</li>
<li><p>reducing dependency direction problems,</p>
</li>
<li><p>extracting cohesive application behavior,</p>
</li>
<li><p>using characterization tests throughout the refactor,</p>
</li>
<li><p>using AI without letting it redesign the system blindly,</p>
</li>
<li><p>and knowing when the application is ready to start migrating.</p>
</li>
</ul>
<p>The examples use TypeScript, but the process applies to most languages and architectures.</p>
<p>The objective is not:</p>
<pre><code class="language-text">legacy application
↓
perfect architecture
</code></pre>
<p>Instead, it's:</p>
<pre><code class="language-text">legacy application
↓
migration-friendly structure
↓
incremental migration
</code></pre>
<p>That difference can save a lot of unnecessary work.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>You should be comfortable with:</p>
<ul>
<li><p>reading an existing codebase</p>
</li>
<li><p>TypeScript or a similar language</p>
</li>
<li><p>unit and integration testing</p>
</li>
<li><p>dependency injection</p>
</li>
<li><p>interfaces and adapters</p>
</li>
<li><p>basic software architecture</p>
</li>
<li><p>incremental refactoring</p>
</li>
</ul>
<p>You should also have some behavioral protection around the capability you plan to modify.</p>
<p>That may include:</p>
<ul>
<li><p>characterization tests</p>
</li>
<li><p>integration tests</p>
</li>
<li><p>contract tests</p>
</li>
</ul>
<p>or another reliable way to verify existing behavior.</p>
<p>Refactoring without that protection is possible. But it's also much harder to distinguish a structural improvement from an accidental behavioral change.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-why-migration-problems-often-start-before-the-migration">Why Migration Problems Often Start Before the Migration</a></p>
</li>
<li><p><a href="#heading-choose-a-migration-boundary-before-refactoring">Choose a Migration Boundary Before Refactoring</a></p>
</li>
<li><p><a href="#heading-dont-refactor-the-entire-application">Don't Refactor the Entire Application</a></p>
</li>
<li><p><a href="#heading-separate-business-rules-from-infrastructure">Separate Business Rules from Infrastructure</a></p>
</li>
<li><p><a href="#heading-introduce-seams-around-hard-dependencies">Introduce Seams Around Hard Dependencies</a></p>
</li>
<li><p><a href="#heading-isolate-side-effects-from-decision-logic">Isolate Side Effects from Decision Logic</a></p>
</li>
<li><p><a href="#heading-put-external-systems-behind-adapters">Put External Systems Behind Adapters</a></p>
</li>
<li><p><a href="#heading-improve-dependency-direction-without-rebuilding-everything">Improve Dependency Direction Without Rebuilding Everything</a></p>
</li>
<li><p><a href="#heading-extract-a-cohesive-application-boundary">Extract a Cohesive Application Boundary</a></p>
</li>
<li><p><a href="#heading-keep-behavioral-tests-running-during-the-refactor">Keep Behavioral Tests Running During the Refactor</a></p>
</li>
<li><p><a href="#heading-how-to-use-ai-during-structural-refactoring">How to Use AI During Structural Refactoring</a></p>
</li>
<li><p><a href="#heading-dont-ask-the-ai-to-design-the-target-architecture-too-early">Don't Ask AI to Design the Target Architecture Too Early</a></p>
</li>
<li><p><a href="#heading-how-to-know-when-a-capability-is-ready-to-migrate">How to Know When a Capability Is Ready to Migrate</a></p>
</li>
<li><p><a href="#heading-a-practical-pre-migration-refactoring-workflow">A Practical Pre-Migration Refactoring Workflow</a></p>
</li>
<li><p><a href="#heading-what-not-to-refactor-before-migration">What Not to Refactor Before Migration</a></p>
</li>
<li><p><a href="#heading-refactoring-is-preparation-not-the-migration">Refactoring Is Preparation, Not the Migration</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-why-migration-problems-often-start-before-the-migration">Why Migration Problems Often Start Before the Migration</h2>
<p>Imagine you need to migrate an order-processing application.</p>
<p>You inspect the main service and find something like this:</p>
<pre><code class="language-typescript">async function processOrder(orderId: string) {
  const connection = await mysql.getConnection();

  const [rows] = await connection.query(
    "SELECT * FROM orders WHERE id = ?",
    [orderId]
  );

  const order = rows[0];

  if (!order) {
    throw new Error("Order not found");
  }

  if (order.customer_type === "PREMIUM") {
    order.total = order.total * 0.9;
  }

  if (
    order.country === "AR" &amp;&amp;
    order.payment_method === "TRANSFER"
  ) {
    order.total -= 500;
  }

  await connection.query(
    "UPDATE orders SET total = ?, status = ? WHERE id = ?",
    [order.total, "PROCESSED", order.id]
  );

  await paymentProvider.createPayment({
    orderId: order.id,
    amount: order.total,
  });

  await eventBus.publish("order.processed", {
    id: order.id,
    total: order.total,
  });

  await emailClient.send({
    to: order.customer_email,
    template: "order-processed",
  });

  return order;
}
</code></pre>
<p>Suppose the migration goal is:</p>
<pre><code class="language-text">MySQL       → PostgreSQL
Old runtime → New runtime
Legacy API  → New service
</code></pre>
<p>The obvious temptation is to begin translating this function into the target stack.</p>
<p>But what exactly are you migrating?</p>
<p>The function contains:</p>
<pre><code class="language-text">database access
business rules
state transition
payment integration
event publication
email delivery
application orchestration
</code></pre>
<p>Changing the database now risks affecting pricing, while changing the payment client risks affecting persistence. And moving the function into another service means moving all of its dependencies at once.</p>
<p>The migration difficulty is partly caused by the current structure. So before migrating, you'll want to create enough separation that those concerns can move independently.</p>
<h2 id="heading-choose-a-migration-boundary-before-refactoring">Choose a Migration Boundary Before Refactoring</h2>
<p>Don't begin with:</p>
<blockquote>
<p>Let's clean up the application.</p>
</blockquote>
<p>Begin with:</p>
<blockquote>
<p>What do we want to migrate first?</p>
</blockquote>
<p>Suppose you decide the first capability will be Process Order. That gives your refactor a boundary.</p>
<p>Now you can map:</p>
<pre><code class="language-text">Input:
orderId

Business behavior:
load order
calculate adjustments
mark as processed

Side effects:
persist order
create payment
publish event
send email

Output:
processed order
</code></pre>
<p>This is much more useful than deciding to refactor:</p>
<pre><code class="language-text">src/services/
</code></pre>
<p>because a folder isn't necessarily a business boundary.</p>
<p>Migration works better when you can reason about capabilities.</p>
<p>For example:</p>
<pre><code class="language-text">Process Order
Cancel Order
Generate Invoice
Register Customer
Renew Subscription
</code></pre>
<p>Each can potentially become a migration unit.</p>
<h2 id="heading-dont-refactor-the-entire-application">Don't Refactor the Entire Application</h2>
<p>Once you start identifying architectural problems, it becomes tempting to fix all of them.</p>
<p>You may notice:</p>
<pre><code class="language-text">circular dependencies
duplicated repositories
global configuration
large services
static helpers
direct database access
inconsistent error handling
mixed domain models
</code></pre>
<p>All of those may deserve attention, but the migration doesn't require all technical debt to disappear.</p>
<p>Suppose your target is the order-processing capability. A useful rule is to refactor only what prevents this capability from moving safely.</p>
<p>For example:</p>
<pre><code class="language-text">Problem:
Order processing calls MySQL directly.

Relevant?
Yes.

Problem:
The reporting module uses inconsistent date formatting.

Relevant?
Probably not.

Problem:
Order processing calls the payment SDK directly.

Relevant?
Yes.

Problem:
The admin UI contains duplicated CSS.

Relevant?
No.
</code></pre>
<p>This prevents preparation from becoming an open-ended cleanup project.</p>
<p>Legacy modernization needs scope discipline.</p>
<h2 id="heading-separate-business-rules-from-infrastructure">Separate Business Rules from Infrastructure</h2>
<p>The most valuable structural change is often separating business behavior from technology-specific details.</p>
<p>Take this code:</p>
<pre><code class="language-typescript">async function processOrder(orderId: string) {
  const order = await mysqlOrders.find(orderId);

  if (order.customerType === "PREMIUM") {
    order.total *= 0.9;
  }

  if (
    order.country === "AR" &amp;&amp;
    order.paymentMethod === "TRANSFER"
  ) {
    order.total -= 500;
  }

  await mysqlOrders.update(order);

  await stripe.createPayment({
    orderId: order.id,
    amount: order.total,
  });
}
</code></pre>
<p>The pricing behavior itself doesn't need MySQL or Stripe.</p>
<p>You can extract it:</p>
<pre><code class="language-typescript">type Order = {
  id: string;
  total: number;
  customerType: "STANDARD" | "PREMIUM";
  country: string;
  paymentMethod: "CARD" | "TRANSFER";
};

function calculateOrderTotal(order: Order): number {
  let total = order.total;

  if (order.customerType === "PREMIUM") {
    total *= 0.9;
  }

  if (
    order.country === "AR" &amp;&amp;
    order.paymentMethod === "TRANSFER"
  ) {
    total -= 500;
  }

  return Math.max(total, 0);
}
</code></pre>
<p>Now:</p>
<pre><code class="language-text">pricing behavior
</code></pre>
<p>is no longer coupled to:</p>
<pre><code class="language-text">MySQL
Stripe
</code></pre>
<p>This doesn't require a complete domain-driven redesign. It's simply a useful separation.</p>
<p>The next migration step can replace infrastructure while leaving this behavior unchanged.</p>
<h2 id="heading-introduce-seams-around-hard-dependencies">Introduce Seams Around Hard Dependencies</h2>
<p>Legacy code often contains dependencies that can't easily be replaced in tests or migration code.</p>
<p>For example:</p>
<pre><code class="language-typescript">class OrderService {
  async process(orderId: string) {
    const client = new LegacyDatabaseClient();

    const order = await client.findOrder(orderId);

    // ...
  }
}
</code></pre>
<p>The database dependency is created inside the method.</p>
<p>That makes substitution difficult.</p>
<p>A small preparatory refactor can introduce a seam:</p>
<pre><code class="language-typescript">interface OrderRepository {
  findById(id: string): Promise&lt;Order | null&gt;;
  save(order: Order): Promise&lt;void&gt;;
}
</code></pre>
<p>Then:</p>
<pre><code class="language-typescript">class OrderService {
  constructor(
    private readonly orders: OrderRepository
  ) {}

  async process(orderId: string) {
    const order = await this.orders.findById(orderId);

    if (!order) {
      throw new Error("Order not found");
    }

    // existing behavior
  }
}
</code></pre>
<p>Now the existing MySQL implementation can satisfy the interface:</p>
<pre><code class="language-typescript">class MySqlOrderRepository implements OrderRepository {
  async findById(id: string) {
    // existing MySQL behavior
  }

  async save(order: Order) {
    // existing MySQL behavior
  }
}
</code></pre>
<p>Later, the migration can introduce:</p>
<pre><code class="language-typescript">class PostgresOrderRepository implements OrderRepository {
  // new implementation
}
</code></pre>
<p>Notice what we didn't change: we didn't change the business behavior. We changed the <strong>replaceability of a dependency</strong>.</p>
<p>That's exactly the kind of refactoring that helps migration.</p>
<h2 id="heading-isolate-side-effects-from-decision-logic">Isolate Side Effects from Decision Logic</h2>
<p>Another useful separation is between:</p>
<pre><code class="language-text">deciding
</code></pre>
<p>and:</p>
<pre><code class="language-text">performing
</code></pre>
<p>Suppose cancellation currently looks like this:</p>
<pre><code class="language-typescript">async function cancelOrder(order: Order) {
  if (order.status === "SHIPPED") {
    throw new Error("Cannot cancel shipped order");
  }

  order.status = "CANCELLED";

  await orders.save(order);
  await inventory.release(order.id);
  await payment.refund(order.id);
  await audit.log("ORDER_CANCELLED", order.id);
}
</code></pre>
<p>There are two different responsibilities here.</p>
<p>The business decision:</p>
<pre><code class="language-text">Can this order be cancelled?
What should its new state be?
</code></pre>
<p>And the operational effects:</p>
<pre><code class="language-text">persist
release inventory
refund
audit
</code></pre>
<p>You could first extract the decision:</p>
<pre><code class="language-typescript">function cancelOrderState(order: Order): Order {
  if (order.status === "SHIPPED") {
    throw new Error("Cannot cancel shipped order");
  }

  return {
    ...order,
    status: "CANCELLED",
  };
}
</code></pre>
<p>Then orchestration remains:</p>
<pre><code class="language-typescript">async function cancelOrder(order: Order) {
  const cancelled = cancelOrderState(order);

  await orders.save(cancelled);
  await inventory.release(cancelled.id);
  await payment.refund(cancelled.id);
  await audit.log("ORDER_CANCELLED", cancelled.id);

  return cancelled;
}
</code></pre>
<p>The behavior is still the same, but now the state transition can be tested and migrated independently.</p>
<p>That matters if the target architecture changes how side effects are executed.</p>
<p>For example, the future version might use:</p>
<pre><code class="language-text">transactional outbox
event-driven workflow
queue
workflow engine
</code></pre>
<p>You don't need to introduce those mechanisms yet. You only need to stop the current decision logic from depending directly on them.</p>
<h2 id="heading-put-external-systems-behind-adapters">Put External Systems Behind Adapters</h2>
<p>External SDKs often leak deeply into legacy code.</p>
<p>For example:</p>
<pre><code class="language-typescript">const result = await stripe.paymentIntents.create({
  amount: order.total,
  currency: "usd",
  metadata: {
    orderId: order.id,
  },
});
</code></pre>
<p>If dozens of application modules depend directly on the Stripe SDK, replacing or relocating payment processing becomes difficult.</p>
<p>Create an application-level boundary instead:</p>
<pre><code class="language-typescript">type PaymentRequest = {
  orderId: string;
  amount: number;
};

type PaymentResult = {
  paymentId: string;
};

interface PaymentGateway {
  charge(
    request: PaymentRequest
  ): Promise&lt;PaymentResult&gt;;
}
</code></pre>
<p>The Stripe adapter contains the provider-specific details:</p>
<pre><code class="language-typescript">class StripePaymentGateway implements PaymentGateway {
  async charge(
    request: PaymentRequest
  ): Promise&lt;PaymentResult&gt; {
    const result =
      await stripe.paymentIntents.create({
        amount: request.amount,
        currency: "usd",
        metadata: {
          orderId: request.orderId,
        },
      });

    return {
      paymentId: result.id,
    };
  }
}
</code></pre>
<p>The application now knows about:</p>
<pre><code class="language-text">PaymentGateway
</code></pre>
<p>instead of:</p>
<pre><code class="language-text">Stripe SDK
</code></pre>
<p>This is useful for migration because provider-specific code is localized.</p>
<p>The same pattern works for:</p>
<pre><code class="language-text">email providers
message brokers
cloud storage
ERP integrations
CRM APIs
identity providers
search engines
</code></pre>
<p>The adapter isn't valuable because interfaces are fashionable. It's valuable because it creates a boundary you can move.</p>
<h2 id="heading-improve-dependency-direction-without-rebuilding-everything">Improve Dependency Direction Without Rebuilding Everything</h2>
<p>Legacy systems often have dependency relationships such as:</p>
<pre><code class="language-text">business logic
    ↓
database SDK
    ↓
framework utilities
</code></pre>
<p>That makes infrastructure difficult to replace.</p>
<p>You don't necessarily need to implement full Clean Architecture. You only need to improve dependency direction where migration requires it.</p>
<p>For example:</p>
<p>Before:</p>
<pre><code class="language-text">OrderService
   ↓
MySQL
</code></pre>
<p>After:</p>
<pre><code class="language-text">OrderService
   ↓
OrderRepository
   ↑
MySqlOrderRepository
</code></pre>
<p>The application depends on an abstraction. The infrastructure implements it.</p>
<p>The same can happen with payments:</p>
<pre><code class="language-text">OrderService
   ↓
PaymentGateway
   ↑
StripePaymentGateway
</code></pre>
<p>and messaging:</p>
<pre><code class="language-text">OrderService
   ↓
OrderEvents
   ↑
KafkaOrderEvents
</code></pre>
<p>Now replacing infrastructure no longer requires rewriting the application service. That's the important outcome.</p>
<h2 id="heading-extract-a-cohesive-application-boundary">Extract a Cohesive Application Boundary</h2>
<p>After several small refactors, the capability may start to look like this:</p>
<pre><code class="language-typescript">interface OrderRepository {
  findById(id: string): Promise&lt;Order | null&gt;;
  save(order: Order): Promise&lt;void&gt;;
}

interface PaymentGateway {
  charge(request: {
    orderId: string;
    amount: number;
  }): Promise&lt;void&gt;;
}

interface OrderEvents {
  processed(order: Order): Promise&lt;void&gt;;
}

class ProcessOrder {
  constructor(
    private readonly orders: OrderRepository,
    private readonly payments: PaymentGateway,
    private readonly events: OrderEvents
  ) {}

  async execute(orderId: string) {
    const order = await this.orders.findById(orderId);

    if (!order) {
      throw new Error("Order not found");
    }

    const total = calculateOrderTotal(order);

    const processed: Order = {
      ...order,
      total,
      status: "PROCESSED",
    };

    await this.orders.save(processed);

    await this.payments.charge({
      orderId: processed.id,
      amount: processed.total,
    });

    await this.events.processed(processed);

    return processed;
  }
}
</code></pre>
<p>This isn't necessarily the final architecture. That's important.</p>
<p>We aren't claiming:</p>
<blockquote>
<p>This is how the application should look forever.</p>
</blockquote>
<p>We're just saying:</p>
<blockquote>
<p>This capability now has boundaries that make migration easier.</p>
</blockquote>
<p>The infrastructure can change independently.</p>
<p>The business rules are testable. The orchestration is visible. And the external contracts are explicit.</p>
<p>That's enough to start considering migration.</p>
<h2 id="heading-keep-behavioral-tests-running-during-the-refactor">Keep Behavioral Tests Running During the Refactor</h2>
<p>This is where the characterization tests from the previous step become useful.</p>
<p>Suppose the original behavior was protected with:</p>
<pre><code class="language-typescript">it("preserves premium order processing behavior", async () =&gt; {
  const result = await processOrder("order-1");

  expect(result.total).toBe(9000);
  expect(result.status).toBe("PROCESSED");

  expect(payment.charge).toHaveBeenCalledWith({
    orderId: "order-1",
    amount: 9000,
  });

  expect(events.processed).toHaveBeenCalled();
});
</code></pre>
<p>Now you can change:</p>
<pre><code class="language-text">direct database access
</code></pre>
<p>into:</p>
<pre><code class="language-text">repository
</code></pre>
<p>and run the test.</p>
<p>Then change:</p>
<pre><code class="language-text">direct payment SDK
</code></pre>
<p>into:</p>
<pre><code class="language-text">payment adapter
</code></pre>
<p>and run the test.</p>
<p>Then extract:</p>
<pre><code class="language-text">pricing logic
</code></pre>
<p>and run the test.</p>
<p>The rhythm becomes:</p>
<pre><code class="language-text">small structural change
↓
test
↓
small structural change
↓
test
↓
small structural change
↓
test
</code></pre>
<p>This matters because structural refactoring is much easier to reason about when behavioral changes aren't happening at the same time.</p>
<p>If a test fails after one small change, the possible cause is narrow.</p>
<p>If a test fails after a two-week rewrite, the possible cause is almost everything.</p>
<h2 id="heading-how-to-use-ai-during-structural-refactoring">How to Use AI During Structural Refactoring</h2>
<p>AI can help a lot during this phase.</p>
<p>But the useful prompts are different from:</p>
<pre><code class="language-text">Refactor this application using Clean Architecture.
</code></pre>
<p>Instead, give the model a constrained transformation.</p>
<p>For example:</p>
<pre><code class="language-text">This service currently accesses MySQL directly.

I want to introduce an OrderRepository seam without
changing observable behavior.

Tasks:

1. identify every database operation used by this service,
2. propose the smallest repository interface needed,
3. move existing database calls behind an adapter,
4. preserve return values, errors, and call order where relevant,
5. do not change business rules,
6. do not introduce additional abstractions.

Explain every structural change before generating code.
</code></pre>
<p>That gives AI a much narrower job.</p>
<p>Another useful request is:</p>
<pre><code class="language-text">Compare the implementation before and after this refactor.

Identify any observable behavior that may have changed.

Check specifically:

- exceptions,
- return values,
- side effects,
- ordering of side effects,
- null handling,
- transaction boundaries,
- retry behavior.

Do not assume equivalence because the code looks similar.
</code></pre>
<p>This is where AI can be valuable as a second reviewer.</p>
<p>It can inspect differences faster than you can manually scan large changes. But the tests still provide stronger evidence.</p>
<h2 id="heading-dont-ask-ai-to-design-the-target-architecture-too-early">Don't Ask AI to Design the Target Architecture Too Early</h2>
<p>AI is very good at recognizing common architecture patterns. But that can also be dangerous.</p>
<p>Give a model a large legacy service and ask:</p>
<pre><code class="language-text">How should this be modernized?
</code></pre>
<p>and you may receive:</p>
<pre><code class="language-text">microservices
event-driven architecture
CQRS
repository pattern
domain events
message broker
API gateway
distributed cache
</code></pre>
<p>All of those are legitimate technologies or patterns, but none of them are automatically justified.</p>
<p>Before choosing a target architecture, you need constraints.</p>
<p>For example:</p>
<pre><code class="language-text">deployment frequency
team size
transactional requirements
latency
failure tolerance
data ownership
integration boundaries
operational maturity
traffic
cost
regulatory requirements
</code></pre>
<p>A monolith with good boundaries may be a better target than microservices. A synchronous workflow may be better than event-driven processing. And a database migration may not require changing the domain model.</p>
<p>Architecture should follow constraints, not pattern recognition.</p>
<p>Use AI to evaluate options. Don't let the presence of a familiar pattern become the reason to adopt it.</p>
<h2 id="heading-how-to-know-when-a-capability-is-ready-to-migrate">How to Know When a Capability Is Ready to Migrate</h2>
<p>At some point, you have to stop refactoring. And that decision matters.</p>
<p>You don't need perfect code. A capability is usually much closer to migration-ready when you can answer these questions clearly.</p>
<h3 id="heading-can-i-describe-its-inputs">Can I Describe its Inputs?</h3>
<p>For example:</p>
<pre><code class="language-text">orderId
customer
request payload
event
</code></pre>
<h3 id="heading-can-i-describe-its-outputs">Can I Describe its Outputs?</h3>
<p>For example:</p>
<pre><code class="language-text">processed order
HTTP response
event
database change
</code></pre>
<h3 id="heading-are-its-important-business-rules-visible">Are its Important Business Rules Visible?</h3>
<p>They don't have to be perfect, but you should know where they live.</p>
<h3 id="heading-are-external-dependencies-explicit">Are External Dependencies Explicit?</h3>
<p>For example:</p>
<pre><code class="language-text">OrderRepository
PaymentGateway
OrderEvents
EmailSender
</code></pre>
<h3 id="heading-can-infrastructure-be-substituted">Can Infrastructure Be Substituted?</h3>
<p>If replacing MySQL requires changing pricing logic, the boundary is probably not ready.</p>
<h3 id="heading-are-important-behaviors-protected">Are Important Behaviors Protected?</h3>
<p>You should have enough tests to detect accidental changes.</p>
<h3 id="heading-do-you-know-the-side-effects">Do You Know the Side Effects?</h3>
<p>For example:</p>
<pre><code class="language-text">persist order
create payment
publish event
send email
</code></pre>
<h3 id="heading-are-major-unknowns-documented">Are Major Unknowns Documented?</h3>
<p>Some uncertainty may remain. But it shouldn't be invisible.</p>
<p>If you can answer those questions, you probably have enough structure to begin migrating that capability.</p>
<h2 id="heading-a-practical-pre-migration-refactoring-workflow">A Practical Pre-Migration Refactoring Workflow</h2>
<p>Here's the workflow I would use.</p>
<h3 id="heading-1-choose-one-capability">1. Choose One Capability</h3>
<p>Don't refactor the whole application.</p>
<p>Pick:</p>
<pre><code class="language-text">Process Order
Generate Invoice
Renew Subscription
</code></pre>
<h3 id="heading-2-confirm-behavioral-protection">2. Confirm Behavioral Protection</h3>
<p>Before structural changes, make sure critical behavior has tests.</p>
<p>Capture:</p>
<pre><code class="language-text">outputs
state transitions
side effects
errors
contracts
</code></pre>
<h3 id="heading-3-identify-migration-blockers">3. Identify Migration Blockers</h3>
<p>Look for coupling such as:</p>
<pre><code class="language-text">direct database access
provider SDKs
global state
framework-specific objects
static dependencies
shared mutable state
</code></pre>
<h3 id="heading-4-extract-pure-business-logic-where-possible">4. Extract Pure Business Logic Where Possible</h3>
<p>Move calculations and decisions away from infrastructure.</p>
<p>For example:</p>
<pre><code class="language-text">calculate price
validate transition
choose status
calculate commission
</code></pre>
<h3 id="heading-5-introduce-seams">5. Introduce Seams</h3>
<p>Create minimal boundaries around:</p>
<pre><code class="language-text">database
payments
events
email
storage
external APIs
</code></pre>
<p>Don't create abstractions without a migration reason.</p>
<h3 id="heading-6-localize-infrastructure">6. Localize Infrastructure</h3>
<p>Move technology-specific behavior into adapters.</p>
<p>For example:</p>
<pre><code class="language-text">MySqlOrderRepository
StripePaymentGateway
KafkaOrderEvents
SendGridEmailSender
</code></pre>
<h3 id="heading-7-make-orchestration-visible">7. Make Orchestration Visible</h3>
<p>Aim for a capability where the sequence is understandable:</p>
<pre><code class="language-text">load
↓
decide
↓
persist
↓
perform side effects
↓
return
</code></pre>
<h3 id="heading-8-run-behavioral-tests-after-every-step">8. Run Behavioral Tests After Every Step</h3>
<p>Don't batch ten refactors together. Keep the changes small.</p>
<h3 id="heading-9-compare-before-and-after">9. Compare Before and After</h3>
<p>Check:</p>
<pre><code class="language-text">inputs
outputs
errors
side effects
data shapes
ordering
transactions
</code></pre>
<h3 id="heading-10-stop-when-migration-becomes-possible">10. Stop When Migration Becomes Possible</h3>
<p>Don't continue refactoring because the code could still be cleaner. It always could.</p>
<p>The objective is migration readiness.</p>
<h2 id="heading-what-not-to-refactor-before-migration">What Not to Refactor Before Migration</h2>
<p>There are several things I would usually avoid changing during this phase unless they directly block migration.</p>
<h3 id="heading-naming-everywhere">Naming Everywhere</h3>
<p>You may dislike hundreds of old names. But renaming everything produces large diffs with little migration value.</p>
<h3 id="heading-formatting-the-entire-repository">Formatting the Entire Repository</h3>
<p>Same problem. Noise makes behavioral changes harder to review.</p>
<h3 id="heading-replacing-every-pattern">Replacing Every Pattern</h3>
<p>A legacy system may contain:</p>
<pre><code class="language-text">singletons
service locators
static utilities
large classes
</code></pre>
<p>Some may remain temporarily. Fix the ones crossing your migration boundary.</p>
<h3 id="heading-rewriting-stable-algorithms">Rewriting Stable Algorithms</h3>
<p>If an old calculation is ugly but protected and isolated, it may be safer to move it first and improve it later.</p>
<h3 id="heading-fixing-every-discovered-bug">Fixing Every Discovered Bug</h3>
<p>This one is especially important.</p>
<p>If you discover a bug while preparing a migration, record it. Then decide whether fixing it belongs in the same change.</p>
<p>Mixing:</p>
<pre><code class="language-text">structural refactor
+
behavioral correction
+
platform migration
</code></pre>
<p>makes failures much harder to understand.</p>
<p>Sometimes the right answer is:</p>
<pre><code class="language-text">preserve bug
migrate
fix bug intentionally afterward
</code></pre>
<p>That sounds uncomfortable. But accidental behavior changes during migration can be much more dangerous.</p>
<h2 id="heading-refactoring-is-preparation-not-the-migration">Refactoring Is Preparation, Not the Migration</h2>
<p>It's easy for pre-migration refactoring to become an endless architecture project.</p>
<p>You start with:</p>
<blockquote>
<p>We need to isolate the database.</p>
</blockquote>
<p>Then:</p>
<blockquote>
<p>We should redesign the domain model.</p>
</blockquote>
<p>Then:</p>
<blockquote>
<p>Maybe we should introduce events.</p>
</blockquote>
<p>Then:</p>
<blockquote>
<p>If we're doing that, maybe this should become a microservice.</p>
</blockquote>
<p>Months later, nothing has migrated. The refactor has become the project.</p>
<p>That's a failure mode, too. The objective should remain concrete.</p>
<p>Before:</p>
<pre><code class="language-text">ProcessOrder
├── MySQL
├── pricing rules
├── Stripe
├── Kafka
├── email
└── framework internals
</code></pre>
<p>After:</p>
<pre><code class="language-text">ProcessOrder
├── OrderRepository
├── pricing rules
├── PaymentGateway
├── OrderEvents
└── EmailSender
</code></pre>
<p>That may be enough.</p>
<p>Now you have choices.</p>
<p>You can migrate:</p>
<pre><code class="language-text">MySQL → PostgreSQL
</code></pre>
<p>without redesigning pricing.</p>
<p>You can replace:</p>
<pre><code class="language-text">Stripe adapter
</code></pre>
<p>without changing order orchestration.</p>
<p>You can move:</p>
<pre><code class="language-text">ProcessOrder
</code></pre>
<p>into another runtime while preserving its contracts.</p>
<p>The refactor created options. That's the value.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Legacy migrations become risky when several types of change happen at once.</p>
<p>You change:</p>
<pre><code class="language-text">behavior
architecture
infrastructure
runtime
data
deployment
</code></pre>
<p>and then try to understand which change caused the failure.</p>
<p>A safer approach is to reduce that uncertainty before migration begins.</p>
<p>First understand the capability, then characterize its behavior, and then change its structure without intentionally changing what it does.</p>
<p>Create boundaries around dependencies. Separate business decisions from infrastructure. Localize external systems. Keep side effects visible. Run behavioral tests after every structural change. And stop refactoring when the capability becomes movable.</p>
<p>The sequence becomes:</p>
<pre><code class="language-text">Understand
↓
Characterize
↓
Refactor
↓
Migrate
</code></pre>
<p>AI can make the refactoring phase dramatically faster.</p>
<p>It can identify dependencies, extract interfaces, move calls behind adapters, compare implementations, and review large diffs.</p>
<p>But faster refactoring doesn't remove the need for architectural judgment. It makes that judgment more important.</p>
<p>Because the goal isn't to produce the cleanest version of the legacy system. The goal is to create <strong>just enough structure to move it safely</strong>.</p>
<p>And once you can change the infrastructure without changing the behavior, migration stops looking like a rewrite.</p>
<p>It starts looking like a sequence of controlled changes.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build Characterization Tests Before Refactoring Legacy Code ]]>
                </title>
                <description>
                    <![CDATA[ The first thing many engineers want to do when they inherit legacy code is improve it. You find a function that's difficult to understand. Or you see duplicated logic, deeply nested conditions, databa ]]>
                </description>
                <link>https://www.freecodecamp.org/news/characterization-tests-before-refactoring-legacy-code/</link>
                <guid isPermaLink="false">6a958d8401db3f18f07d0b54</guid>
                
                    <category>
                        <![CDATA[ Software Testing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ refactoring ]]>
                    </category>
                
                    <category>
                        <![CDATA[ legacy code ]]>
                    </category>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Artificial Intelligence ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Hugo Teijiz ]]>
                </dc:creator>
                <pubDate>Mon, 31 Aug 2026 14:19:48 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/50ec1fe6-8e1f-4c42-a8ad-fd52ea0d089d.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>The first thing many engineers want to do when they inherit legacy code is improve it.</p>
<p>You find a function that's difficult to understand. Or you see duplicated logic, deeply nested conditions, database calls mixed with business rules, and dependencies that make testing almost impossible.</p>
<p>You know the code could be better, so you start cleaning it up.</p>
<p>Then something breaks. Not because the new implementation is obviously wrong. It breaks because the old implementation was doing something nobody knew it was doing.</p>
<p>That's one of the most common risks in legacy modernization.</p>
<p>Before changing code, you need a way to answer a simple question:</p>
<blockquote>
<p>Did I preserve the behavior that already mattered?</p>
</blockquote>
<p>That is where characterization tests become useful.</p>
<p>A characterization test doesn't begin by asking what the software <strong>should</strong> do. It begins by documenting what the software <strong>does today</strong>.</p>
<p>That distinction matters.</p>
<p>In a greenfield application, tests usually express intended behavior. But in a legacy application, you may first need tests that capture existing behavior so you can change the implementation without accidentally changing its observable results.</p>
<p>In this tutorial, I'll show you how to use characterization tests as a safety net before refactoring legacy code.</p>
<p>We'll look at how to:</p>
<ul>
<li><p>identify behavior worth protecting,</p>
</li>
<li><p>choose useful test boundaries,</p>
</li>
<li><p>capture current outputs,</p>
</li>
<li><p>deal with side effects,</p>
</li>
<li><p>handle databases and external systems,</p>
</li>
<li><p>use AI to accelerate test discovery,</p>
</li>
<li><p>avoid freezing implementation details,</p>
</li>
<li><p>decide what not to characterize,</p>
</li>
<li><p>and turn characterization tests into a foundation for safer refactoring.</p>
</li>
</ul>
<p>The examples use TypeScript and Vitest, but the approach applies to most languages and testing frameworks.</p>
<p>The goal isn't to preserve every line of legacy behavior forever. The goal is to make behavior visible before you start changing the code that produces it.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>You should be comfortable with:</p>
<ul>
<li><p>TypeScript or a similar programming language</p>
</li>
<li><p>unit and integration testing</p>
</li>
<li><p>dependency injection</p>
</li>
<li><p>mocks and test doubles</p>
</li>
<li><p>basic refactoring techniques</p>
</li>
<li><p>reading an unfamiliar codebase</p>
</li>
</ul>
<p>It also helps if you've already mapped the capability you want to change.</p>
<p>Before writing characterization tests, you should have some idea of:</p>
<ul>
<li><p>where the behavior starts</p>
</li>
<li><p>what state it changes</p>
</li>
<li><p>which external systems it touches</p>
</li>
<li><p>which outputs may be consumed elsewhere</p>
</li>
</ul>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-characterization-tests-actually-protect">What Characterization Tests Actually Protect</a></p>
</li>
<li><p><a href="#heading-start-with-behavior-not-implementation">Start with Behavior, Not Implementation</a></p>
</li>
<li><p><a href="#heading-choose-one-capability-before-writing-tests">Choose One Capability Before Writing Tests</a></p>
</li>
<li><p><a href="#heading-find-the-smallest-useful-test-boundary">Find the Smallest Useful Test Boundary</a></p>
</li>
<li><p><a href="#heading-capture-existing-behavior-before-improving-it">Capture Existing Behavior Before Improving It</a></p>
</li>
<li><p><a href="#heading-characterize-edge-cases-you-dont-yet-understand">Characterize Edge Cases You Don't Yet Understand</a></p>
</li>
<li><p><a href="#heading-test-side-effects-not-just-return-values">Test Side Effects, Not Just Return Values</a></p>
</li>
<li><p><a href="#heading-how-to-characterize-code-that-depends-on-a-database">How to Characterize Code That Depends on a Database</a></p>
</li>
<li><p><a href="#heading-how-to-handle-external-services">How to Handle External Services</a></p>
</li>
<li><p><a href="#heading-how-to-use-ai-to-discover-characterization-tests">How to Use AI to Discover Characterization Tests</a></p>
</li>
<li><p><a href="#heading-dont-let-ai-invent-expected-behavior">Don't Let AI Invent Expected Behavior</a></p>
</li>
<li><p><a href="#heading-avoid-testing-implementation-details">Avoid Testing Implementation Details</a></p>
</li>
<li><p><a href="#heading-when-a-characterization-test-reveals-a-bug">When a Characterization Test Reveals a Bug</a></p>
</li>
<li><p><a href="#heading-how-much-behavior-should-you-characterize">How Much Behavior Should You Characterize</a></p>
</li>
<li><p><a href="#heading-use-characterization-tests-during-the-refactor">Use Characterization Tests During the Refactor</a></p>
</li>
<li><p><a href="#heading-a-practical-characterization-testing-workflow">A Practical Characterization Testing Workflow</a></p>
</li>
<li><p><a href="#heading-what-characterization-tests-cant-tell-you">What Characterization Tests Can't Tell You</a></p>
</li>
<li><p><a href="#heading-characterization-tests-are-temporary-knowledge-infrastructure">Characterization Tests Are Temporary Knowledge Infrastructure</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-characterization-tests-actually-protect">What Characterization Tests Actually Protect</h2>
<p>Suppose you inherit this function:</p>
<pre><code class="language-typescript">type Customer = {
  id: string;
  type: "STANDARD" | "PREMIUM";
};

type Order = {
  id: string;
  customer: Customer;
  subtotal: number;
  country: string;
  paymentMethod: "CARD" | "TRANSFER";
};

async function processOrder(order: Order) {
  let total = order.subtotal;

  if (order.customer.type === "PREMIUM") {
    total = total * 0.9;
  }

  if (order.country === "AR" &amp;&amp; order.paymentMethod === "TRANSFER") {
    total = total - 500;
  }

  if (total &lt; 0) {
    total = 0;
  }

  await ordersRepository.save({
    ...order,
    total,
    status: "PROCESSED",
  });

  await eventBus.publish("order.processed", {
    orderId: order.id,
    total,
  });

  return total;
}
</code></pre>
<p>There are several things you may want to refactor here.</p>
<p>For example, the pricing rules could move to another module. Persistence could be isolated. The event publisher could sit behind an interface. And the function could return an object rather than a primitive.</p>
<p>Those may all be good decisions, but before making them, you should ask: What behavior currently matters?</p>
<p>For this function, observable behavior includes at least:</p>
<ul>
<li><p>premium customers receive a 10% discount</p>
</li>
<li><p>Argentine transfers receive another adjustment</p>
</li>
<li><p>totals can't become negative</p>
</li>
<li><p>the order is persisted with a specific status</p>
</li>
<li><p>an event is published</p>
</li>
<li><p>the event contains the calculated total</p>
</li>
<li><p>the function returns that total</p>
</li>
</ul>
<p>A characterization test gives you a baseline for those behaviors.</p>
<p>For example:</p>
<pre><code class="language-typescript">import { describe, expect, it, vi } from "vitest";

describe("processOrder", () =&gt; {
  it("applies the existing premium customer behavior", async () =&gt; {
    const save = vi.spyOn(ordersRepository, "save");
    const publish = vi.spyOn(eventBus, "publish");

    const order: Order = {
      id: "order-1",
      customer: {
        id: "customer-1",
        type: "PREMIUM",
      },
      subtotal: 10000,
      country: "US",
      paymentMethod: "CARD",
    };

    const result = await processOrder(order);

    expect(result).toBe(9000);

    expect(save).toHaveBeenCalledWith(
      expect.objectContaining({
        id: "order-1",
        total: 9000,
        status: "PROCESSED",
      })
    );

    expect(publish).toHaveBeenCalledWith("order.processed", {
      orderId: "order-1",
      total: 9000,
    });
  });
});
</code></pre>
<p>This test isn't saying that a 10% discount is the best pricing model.</p>
<p>It's saying:</p>
<blockquote>
<p>This is what the system currently does.</p>
</blockquote>
<p>That's the contract you need to understand before changing it.</p>
<h2 id="heading-start-with-behavior-not-implementation">Start with Behavior, Not Implementation</h2>
<p>A common mistake is to write tests around the structure you're planning to create.</p>
<p>Suppose you want to refactor the previous code into:</p>
<pre><code class="language-text">OrderProcessor
PricingPolicy
OrdersRepository
OrderEventPublisher
</code></pre>
<p>You may be tempted to write tests for those future classes first.</p>
<p>But those classes don't describe the existing system. They describe your proposed design.</p>
<p>Characterization tests should begin at the current observable boundary.</p>
<p>Instead of asking:</p>
<blockquote>
<p>How should <code>PricingPolicy</code> work?</p>
</blockquote>
<p>ask:</p>
<blockquote>
<p>Given this input, what does <code>processOrder()</code> currently produce?</p>
</blockquote>
<p>That difference helps prevent your new architecture from redefining behavior accidentally.</p>
<p>The sequence should be:</p>
<pre><code class="language-text">Observe existing behavior
↓
Capture it
↓
Refactor implementation
↓
Run characterization tests
↓
Verify behavior remains stable
</code></pre>
<p>Not:</p>
<pre><code class="language-text">Design new architecture
↓
Write tests for new architecture
↓
Assume it matches the old system
</code></pre>
<p>The second workflow tests your design. The first protects the migration.</p>
<h2 id="heading-choose-one-capability-before-writing-tests">Choose One Capability Before Writing Tests</h2>
<p>Don't start by trying to characterize an entire legacy application. Instead, pick one business capability.</p>
<p>For example:</p>
<pre><code class="language-text">Approve Order
Generate Invoice
Renew Subscription
Register Customer
Calculate Commission
Cancel Reservation
</code></pre>
<p>Then trace that capability through the system.</p>
<p>Suppose you choose:</p>
<blockquote>
<p>Generate Invoice</p>
</blockquote>
<p>You discover this path:</p>
<pre><code class="language-text">POST /orders/:id/invoice
        ↓
InvoiceController.generate()
        ↓
InvoiceService.generate()
        ↓
TaxCalculator.calculate()
        ↓
InvoiceRepository.save()
        ↓
PdfGenerator.create()
        ↓
EmailService.send()
</code></pre>
<p>That becomes the scope of your investigation.</p>
<p>Now ask: Which behaviors matter if I refactor this capability?</p>
<p>Perhaps:</p>
<pre><code class="language-text">tax calculation
invoice numbering
database state
PDF fields
email recipient
email attachment
error behavior
</code></pre>
<p>Those are candidates for characterization.</p>
<p>This is more useful than trying to increase test coverage across the repository indiscriminately.</p>
<p>Coverage isn't the goal. Behavioral confidence is.</p>
<h2 id="heading-find-the-smallest-useful-test-boundary">Find the Smallest Useful Test Boundary</h2>
<p>Characterization tests can exist at different levels.</p>
<p>You might test:</p>
<pre><code class="language-text">function
service
module
API endpoint
background job
complete workflow
</code></pre>
<p>The right boundary is usually the smallest one that still captures meaningful behavior.</p>
<p>Suppose the logic you want to refactor lives inside:</p>
<pre><code class="language-typescript">class InvoiceService {
  async generate(orderId: string) {
    // 300 lines of legacy behavior
  }
}
</code></pre>
<p>If <code>generate()</code> coordinates tax calculation, persistence, numbering, and external calls, testing a small internal helper may not protect enough behavior.</p>
<p>Testing the whole production stack may be too slow and difficult.</p>
<p>A service-level characterization test may be the useful compromise.</p>
<p>For example:</p>
<pre><code class="language-typescript">describe("InvoiceService.generate", () =&gt; {
  it("preserves the existing invoice calculation", async () =&gt; {
    const service = createInvoiceService();

    const invoice = await service.generate("order-123");

    expect(invoice.subtotal).toBe(10000);
    expect(invoice.tax).toBe(2100);
    expect(invoice.total).toBe(12100);
  });
});
</code></pre>
<p>You don't want to ask what's the smallest unit you can test. You want to ask what's the smallest boundary that gives you confidence during this refactor.</p>
<p>Those aren't always the same thing.</p>
<h2 id="heading-capture-existing-behavior-before-improving-it">Capture Existing Behavior Before Improving It</h2>
<p>Legacy code often contains behavior that looks suspicious.</p>
<p>Consider:</p>
<pre><code class="language-typescript">function calculateDiscount(amount: number) {
  if (amount &gt; 10000) {
    return amount * 0.15;
  }

  if (amount &gt; 5000) {
    return amount * 0.1;
  }

  return 0;
}
</code></pre>
<p>You run a few examples and discover:</p>
<pre><code class="language-text">5000  -&gt; 0
5001  -&gt; 500.1
10000 -&gt; 1000
10001 -&gt; 1500.15
</code></pre>
<p>You might think:</p>
<blockquote>
<p><code>5000</code> should probably receive the 10% discount.</p>
</blockquote>
<p>Maybe. But that's not what the current code does.</p>
<p>A characterization test could record:</p>
<pre><code class="language-typescript">describe("calculateDiscount", () =&gt; {
  it.each([
    [5000, 0],
    [5001, 500.1],
    [10000, 1000],
    [10001, 1500.15],
  ])(
    "returns the existing discount for amount %d",
    (amount, expected) =&gt; {
      expect(calculateDiscount(amount)).toBe(expected);
    }
  );
});
</code></pre>
<p>This creates a behavioral boundary around the existing implementation.</p>
<p>Later, if the business confirms that <code>5000</code> should receive a discount, you can intentionally change:</p>
<pre><code class="language-typescript">if (amount &gt; 5000)
</code></pre>
<p>to:</p>
<pre><code class="language-typescript">if (amount &gt;= 5000)
</code></pre>
<p>and update the relevant test.</p>
<p>The important part is that the change becomes explicit.</p>
<p>Without the test, it could happen accidentally during an unrelated refactor.</p>
<h2 id="heading-characterize-edge-cases-you-dont-yet-understand">Characterize Edge Cases You Don't Yet Understand</h2>
<p>The obvious cases aren't always the risky ones. Legacy systems often fail at boundaries.</p>
<p>Look for values such as:</p>
<pre><code class="language-text">0
-1
null
empty string
maximum value
minimum value
exact threshold values
unknown status
duplicate identifiers
missing related records
</code></pre>
<p>Suppose you find:</p>
<pre><code class="language-typescript">function normalizeBalance(balance?: number) {
  if (!balance) {
    return 0;
  }

  return Math.round(balance * 100) / 100;
}
</code></pre>
<p>That means:</p>
<pre><code class="language-text">undefined -&gt; 0
0         -&gt; 0
</code></pre>
<p>But also potentially:</p>
<pre><code class="language-text">NaN -&gt; 0
</code></pre>
<p>because <code>NaN</code> is falsy.</p>
<p>Is that intentional? You may not know yet.</p>
<p>You can characterize it:</p>
<pre><code class="language-typescript">describe("normalizeBalance", () =&gt; {
  it("returns zero for undefined", () =&gt; {
    expect(normalizeBalance(undefined)).toBe(0);
  });

  it("returns zero for zero", () =&gt; {
    expect(normalizeBalance(0)).toBe(0);
  });

  it("returns zero for NaN in the current implementation", () =&gt; {
    expect(normalizeBalance(Number.NaN)).toBe(0);
  });
});
</code></pre>
<p>The name matters.</p>
<p>Notice that I wrote:</p>
<blockquote>
<p>in the current implementation</p>
</blockquote>
<p>I'm not pretending that behavior is correct. I'm just documenting it.</p>
<p>That distinction becomes important when a test describes questionable behavior.</p>
<h2 id="heading-test-side-effects-not-just-return-values">Test Side Effects, Not Just Return Values</h2>
<p>A return value is only one kind of behavior.</p>
<p>Legacy functions frequently produce side effects.</p>
<p>Consider:</p>
<pre><code class="language-typescript">async function cancelOrder(order: Order) {
  order.status = "CANCELLED";

  await orders.save(order);
  await inventory.release(order.id);
  await audit.log("ORDER_CANCELLED", order.id);

  return order;
}
</code></pre>
<p>A weak characterization test might only check:</p>
<pre><code class="language-typescript">expect(result.status).toBe("CANCELLED");
</code></pre>
<p>But a refactor could still accidentally remove:</p>
<pre><code class="language-text">inventory.release()
audit.log()
</code></pre>
<p>and the test would continue passing.</p>
<p>A stronger characterization test captures observable side effects:</p>
<pre><code class="language-typescript">it("preserves cancellation side effects", async () =&gt; {
  const save = vi.spyOn(orders, "save");
  const release = vi.spyOn(inventory, "release");
  const log = vi.spyOn(audit, "log");

  const order = {
    id: "order-1",
    status: "APPROVED",
  } as Order;

  await cancelOrder(order);

  expect(save).toHaveBeenCalled();

  expect(release).toHaveBeenCalledWith("order-1");

  expect(log).toHaveBeenCalledWith(
    "ORDER_CANCELLED",
    "order-1"
  );
});
</code></pre>
<p>This doesn't mean every internal call deserves an assertion.</p>
<p>The question is whether the call produces observable behavior that matters outside the implementation.</p>
<h2 id="heading-how-to-characterize-code-that-depends-on-a-database">How to Characterize Code That Depends on a Database</h2>
<p>Database-heavy legacy code can be difficult to test.</p>
<p>Suppose you have:</p>
<pre><code class="language-typescript">async function activateCustomer(customerId: string) {
  const customer = await db.customers.findById(customerId);

  if (!customer) {
    throw new Error("Customer not found");
  }

  await db.customers.update(customerId, {
    status: "ACTIVE",
    activatedAt: new Date(),
  });

  return db.customers.findById(customerId);
}
</code></pre>
<p>You have several options.</p>
<h3 id="heading-use-an-integration-test">Use an Integration Test</h3>
<p>If the database behavior itself matters, run against a disposable test database.</p>
<p>For example:</p>
<pre><code class="language-typescript">it("activates an existing customer", async () =&gt; {
  await seedCustomer({
    id: "customer-1",
    status: "PENDING",
  });

  const result = await activateCustomer("customer-1");

  expect(result?.status).toBe("ACTIVE");
  expect(result?.activatedAt).toBeTruthy();
});
</code></pre>
<p>This gives high confidence, but the test may be slower.</p>
<h3 id="heading-introduce-a-seam">Introduce a Seam</h3>
<p>If database access makes testing impractical, you may need a very small structural change before characterization.</p>
<p>For example:</p>
<pre><code class="language-typescript">type CustomerRepository = {
  findById(id: string): Promise&lt;Customer | null&gt;;
  update(
    id: string,
    data: Partial&lt;Customer&gt;
  ): Promise&lt;void&gt;;
};
</code></pre>
<p>Then:</p>
<pre><code class="language-typescript">async function activateCustomer(
  customerId: string,
  customers: CustomerRepository
) {
  // existing behavior
}
</code></pre>
<p>This is a useful concept from legacy-code work: create a <strong>seam</strong>, a place where behavior can be observed or replaced without rewriting the system.</p>
<p>The key is to keep this preparatory change mechanical.</p>
<p>Don't redesign the business logic while creating the test boundary.</p>
<p>First make it testable. Then characterize it. Then refactor.</p>
<h2 id="heading-how-to-handle-external-services">How to Handle External Services</h2>
<p>Legacy code frequently talks directly to:</p>
<pre><code class="language-text">payment providers
email services
ERPs
CRMs
message brokers
cloud storage
third-party APIs
</code></pre>
<p>You usually don't want characterization tests repeatedly calling those systems.</p>
<p>Instead, capture the interaction at the boundary.</p>
<p>Suppose:</p>
<pre><code class="language-typescript">async function chargeOrder(order: Order) {
  const response = await stripe.charge({
    amount: order.total,
    currency: "usd",
    customerId: order.customerId,
  });

  await orders.markPaid(order.id, response.id);

  return response.id;
}
</code></pre>
<p>You can characterize the request:</p>
<pre><code class="language-typescript">it("sends the existing payment payload", async () =&gt; {
  const charge = vi
    .spyOn(stripe, "charge")
    .mockResolvedValue({
      id: "payment-123",
    });

  const markPaid = vi.spyOn(orders, "markPaid");

  const order = {
    id: "order-1",
    total: 5000,
    customerId: "customer-1",
  } as Order;

  await chargeOrder(order);

  expect(charge).toHaveBeenCalledWith({
    amount: 5000,
    currency: "usd",
    customerId: "customer-1",
  });

  expect(markPaid).toHaveBeenCalledWith(
    "order-1",
    "payment-123"
  );
});
</code></pre>
<p>That protects the external contract without hitting the external system.</p>
<p>But be careful. If the provider behavior itself matters, mocks alone may not be enough.</p>
<p>You might also need:</p>
<ul>
<li><p>provider sandbox tests</p>
</li>
<li><p>contract tests</p>
</li>
<li><p>integration tests</p>
</li>
<li><p>schema validation</p>
</li>
</ul>
<p>Characterization testing doesn't eliminate the need for those layers.</p>
<h2 id="heading-how-to-use-ai-to-discover-characterization-tests">How to Use AI to Discover Characterization Tests</h2>
<p>AI is particularly useful when you are staring at a large legacy function and trying to understand what deserves a test.</p>
<p>Suppose you have a 400-line service.</p>
<p>Instead of asking:</p>
<pre><code class="language-text">Write unit tests for this class.
</code></pre>
<p>use a more investigative prompt:</p>
<pre><code class="language-text">Analyze this class without changing it.

Identify observable behaviors that could change during refactoring.

Group them into:

1. returned values,
2. state changes,
3. persistence effects,
4. external calls,
5. emitted events,
6. exceptions,
7. boundary conditions.

For every proposed characterization test:

- reference the relevant source code,
- explain what behavior the test would protect,
- distinguish observed behavior from inferred behavior.

Do not invent expected values.
</code></pre>
<p>That final instruction matters: you want AI to help identify <strong>what to observe</strong>. You don't want it inventing what the software should do.</p>
<p>Another useful prompt is:</p>
<pre><code class="language-text">Review the existing test suite for this capability.

Compare the behaviors covered by tests with the
observable behaviors in the implementation.

List behavior that appears unprotected.

Do not generate tests yet.
</code></pre>
<p>This is often more valuable than immediately asking for test code.</p>
<p>First identify the gaps, and then decide which gaps matter.</p>
<h2 id="heading-dont-let-ai-invent-expected-behavior">Don't Let AI Invent Expected Behavior</h2>
<p>This is probably the most important rule when combining AI with characterization testing, and it's worth talking a bit more about.</p>
<p>Suppose AI reads:</p>
<pre><code class="language-typescript">if (customer.age &gt; 65) {
  discount = 0.2;
}
</code></pre>
<p>It may generate:</p>
<pre><code class="language-typescript">expect(calculateDiscount(65)).toBe(0.2);
</code></pre>
<p>because it assumes the intended business rule is:</p>
<blockquote>
<p>Customers aged 65 or older receive a discount.</p>
</blockquote>
<p>But that's not what the code says.</p>
<p>The existing behavior is:</p>
<pre><code class="language-text">65 -&gt; no discount
66 -&gt; discount
</code></pre>
<p>The expected values in characterization tests should come from evidence.</p>
<p>Useful evidence includes:</p>
<ul>
<li><p>running the current system</p>
</li>
<li><p>existing tests</p>
</li>
<li><p>fixtures</p>
</li>
<li><p>production-safe observations</p>
</li>
<li><p>documented examples</p>
</li>
<li><p>database state</p>
</li>
<li><p>historical behavior</p>
</li>
</ul>
<p>Don't derive expectations solely from what seems reasonable.</p>
<p>A better AI instruction is:</p>
<pre><code class="language-text">For each candidate test, tell me how I can obtain
the expected result from the current implementation.

Do not propose the expected result yourself unless it
can be directly derived from executable behavior
or an existing test.
</code></pre>
<p>This turns AI into an assistant for experiment design rather than an authority on business rules.</p>
<h2 id="heading-avoid-testing-implementation-details">Avoid Testing Implementation Details</h2>
<p>Characterization tests can become harmful if they freeze the current code structure.</p>
<p>Suppose the implementation is:</p>
<pre><code class="language-typescript">async function processOrder(order: Order) {
  validateOrder(order);
  calculatePrice(order);
  reserveInventory(order);
  saveOrder(order);
}
</code></pre>
<p>A brittle test might assert:</p>
<pre><code class="language-typescript">expect(validateOrder).toHaveBeenCalledBefore(calculatePrice);
expect(calculatePrice).toHaveBeenCalledBefore(reserveInventory);
expect(reserveInventory).toHaveBeenCalledBefore(saveOrder);
</code></pre>
<p>Maybe that order matters. Maybe it doesn't.</p>
<p>If consumers only care about:</p>
<pre><code class="language-text">correct total
inventory reserved
order persisted
</code></pre>
<p>then asserting the exact sequence unnecessarily constrains the refactor.</p>
<p>Prefer protecting externally meaningful behavior.</p>
<p>For example:</p>
<pre><code class="language-typescript">expect(savedOrder.total).toBe(9000);
expect(inventory.reserve).toHaveBeenCalledWith(
  "product-1",
  2
);
expect(repository.save).toHaveBeenCalled();
</code></pre>
<p>Characterization tests should create a safety net. They shouldn't turn the legacy implementation into a specification of every internal decision.</p>
<h2 id="heading-when-a-characterization-test-reveals-a-bug">When a Characterization Test Reveals a Bug</h2>
<p>Eventually you'll encounter behavior that appears clearly wrong.</p>
<p>For example:</p>
<pre><code class="language-typescript">function calculateFee(amount: number) {
  if (amount === 0) {
    return 100;
  }

  return amount * 0.02;
}
</code></pre>
<p>You confirm that zero-value transactions are charged a fixed fee. Everyone agrees this looks suspicious.</p>
<p>What should the characterization test do?</p>
<p>First, separate two questions:</p>
<ol>
<li><p>What does the system do today?</p>
</li>
<li><p>What should the system do?</p>
</li>
</ol>
<p>The characterization test answers the first.</p>
<pre><code class="language-typescript">it("currently charges 100 for a zero-value transaction", () =&gt; {
  expect(calculateFee(0)).toBe(100);
});
</code></pre>
<p>Then investigate whether this is intentional business behavior, a historical workaround, or an actual defect.</p>
<p>If the business confirms it is a bug, create a separate change.</p>
<p>For example:</p>
<pre><code class="language-typescript">it("does not charge a fee for a zero-value transaction", () =&gt; {
  expect(calculateFee(0)).toBe(0);
});
</code></pre>
<p>Then modify the production code.</p>
<p>This may sound overly formal for a small condition. But it creates a clean distinction between:</p>
<pre><code class="language-text">behavior discovered during refactoring
</code></pre>
<p>and:</p>
<pre><code class="language-text">behavior intentionally changed
</code></pre>
<p>That distinction becomes extremely valuable in large migrations.</p>
<h2 id="heading-how-much-behavior-should-you-characterize">How Much Behavior Should You Characterize</h2>
<p>You don't need to characterize everything. Trying to preserve every observed detail can create another form of paralysis.</p>
<p>Prioritize behavior with high change risk or high business impact.</p>
<p>I usually look first at:</p>
<ul>
<li><p>financial calculations</p>
</li>
<li><p>state transitions</p>
</li>
<li><p>authentication and authorization</p>
</li>
<li><p>external contracts</p>
</li>
<li><p>queue and event payloads</p>
</li>
<li><p>data transformations</p>
</li>
<li><p>retry behavior</p>
</li>
<li><p>idempotency</p>
</li>
<li><p>regulatory rules</p>
</li>
<li><p>critical error handling</p>
</li>
</ul>
<p>You may care less about:</p>
<ul>
<li><p>internal helper naming</p>
</li>
<li><p>private method structure</p>
</li>
<li><p>log wording that nobody consumes</p>
</li>
<li><p>temporary object shapes</p>
</li>
<li><p>implementation-specific call sequences</p>
</li>
</ul>
<p>A useful question is: If this behavior changed during refactoring, could somebody outside this function notice?</p>
<p>If the answer is yes, it's probably worth considering.</p>
<h2 id="heading-use-characterization-tests-during-the-refactor">Use Characterization Tests During the Refactor</h2>
<p>Once the characterization suite exists, keep the refactor small.</p>
<p>Suppose you begin with:</p>
<pre><code class="language-typescript">async function processOrder(order: Order) {
  // validation
  // pricing
  // inventory
  // persistence
  // event publishing
}
</code></pre>
<p>You might first extract pricing:</p>
<pre><code class="language-typescript">function calculateOrderTotal(order: Order) {
  let total = order.subtotal;

  if (order.customer.type === "PREMIUM") {
    total *= 0.9;
  }

  if (
    order.country === "AR" &amp;&amp;
    order.paymentMethod === "TRANSFER"
  ) {
    total -= 500;
  }

  return Math.max(total, 0);
}
</code></pre>
<p>Run the characterization suite. If everything still passes, continue.</p>
<p>Next isolate inventory and run it again.</p>
<p>Then persistence. Run it again.</p>
<p>This gives you a migration rhythm:</p>
<pre><code class="language-text">small structural change
↓
run tests
↓
observe
↓
continue
</code></pre>
<p>If something fails, the search space is small.</p>
<p>Compare that with rewriting 2,000 lines and then discovering 47 broken tests.</p>
<p>Small changes turn failures into useful feedback, while Large changes turn failures into archaeology.</p>
<p>Again.</p>
<h2 id="heading-a-practical-characterization-testing-workflow">A Practical Characterization Testing Workflow</h2>
<p>Here is the workflow I would use on an unfamiliar legacy capability.</p>
<h3 id="heading-1-map-the-capability">1. Map the Capability</h3>
<p>Identify:</p>
<pre><code class="language-text">entry point
business logic
state changes
side effects
external contracts
outputs
</code></pre>
<p>Don't refactor yet.</p>
<h3 id="heading-2-find-existing-tests">2. Find Existing Tests</h3>
<p>Search for tests that already describe the capability.</p>
<p>Look for:</p>
<pre><code class="language-text">happy paths
boundary cases
errors
historical bugs
integration behavior
</code></pre>
<p>Don't duplicate useful tests unnecessarily.</p>
<h3 id="heading-3-list-observable-behaviors">3. List Observable Behaviors</h3>
<p>Create a table such as:</p>
<table>
<thead>
<tr>
<th>Behavior</th>
<th>Evidence</th>
<th>Protected?</th>
</tr>
</thead>
<tbody><tr>
<td>Premium discount</td>
<td>Code + production example</td>
<td>No</td>
</tr>
<tr>
<td>Order event</td>
<td>Code</td>
<td>Yes</td>
</tr>
<tr>
<td>Transfer adjustment</td>
<td>Code</td>
<td>No</td>
</tr>
<tr>
<td>Negative total clamp</td>
<td>Code</td>
<td>No</td>
</tr>
<tr>
<td>Save status</td>
<td>Existing integration test</td>
<td>Yes</td>
</tr>
</tbody></table>
<p>Now you know where the risk is.</p>
<h3 id="heading-4-pick-the-test-boundary">4. Pick the Test Boundary</h3>
<p>Decide whether the useful boundary is:</p>
<pre><code class="language-text">function
service
module
endpoint
job
workflow
</code></pre>
<p>Choose based on confidence, not test ideology.</p>
<h3 id="heading-5-capture-current-behavior">5. Capture Current Behavior</h3>
<p>Run the existing system.</p>
<p>Use real observable outputs when possible.</p>
<p>Don't guess expectations.</p>
<h3 id="heading-6-add-critical-edge-cases">6. Add Critical Edge Cases</h3>
<p>Test:</p>
<pre><code class="language-text">thresholds
empty values
nulls
errors
duplicate operations
retry scenarios
</code></pre>
<p>especially around logic you intend to change.</p>
<h3 id="heading-7-capture-side-effects">7. Capture Side Effects</h3>
<p>Protect meaningful:</p>
<pre><code class="language-text">writes
events
messages
external calls
state transitions
</code></pre>
<p>not only function return values.</p>
<h3 id="heading-8-mark-uncertain-behavior">8. Mark Uncertain Behavior</h3>
<p>Use test names or documentation that clearly distinguishes:</p>
<pre><code class="language-text">confirmed business rule
</code></pre>
<p>from:</p>
<pre><code class="language-text">current observed behavior
</code></pre>
<h3 id="heading-9-refactor-incrementally">9. Refactor Incrementally</h3>
<p>Make one structural change.</p>
<p>Run the suite.</p>
<p>Repeat.</p>
<h3 id="heading-10-replace-characterization-with-intent-where-appropriate">10. Replace Characterization with Intent Where Appropriate</h3>
<p>As understanding improves, some characterization tests can evolve into true specification tests.</p>
<p>Instead of:</p>
<pre><code class="language-text">currently returns 0 for this input
</code></pre>
<p>you may eventually be able to say:</p>
<pre><code class="language-text">does not apply a discount below the premium threshold
</code></pre>
<p>That transition is useful. It means the system is becoming understood rather than merely preserved.</p>
<h2 id="heading-what-characterization-tests-cant-tell-you">What Characterization Tests Can't Tell You</h2>
<p>Characterization tests are powerful, but they have an important limitation.</p>
<p>They tell you what happened for the cases you observed. They don't automatically tell you why.</p>
<p>Suppose the test says:</p>
<pre><code class="language-text">Argentine transfer orders receive a 500-unit adjustment.
</code></pre>
<p>The test can protect that behavior.</p>
<p>It can't tell you whether the adjustment exists because of:</p>
<ul>
<li><p>a tax rule</p>
</li>
<li><p>a banking fee</p>
</li>
<li><p>an old promotion</p>
</li>
<li><p>a customer-specific workaround</p>
</li>
<li><p>a bug nobody removed</p>
</li>
</ul>
<p>For that, you still need other evidence:</p>
<ul>
<li><p>documentation</p>
</li>
<li><p>Git history</p>
</li>
<li><p>production telemetry</p>
</li>
<li><p>domain experts</p>
</li>
<li><p>incident records</p>
</li>
<li><p>external system contracts</p>
</li>
</ul>
<p>This is why characterization testing belongs after codebase understanding, not instead of it.</p>
<p>You first discover the behavior. Then you protect it. Then you continue investigating what it means.</p>
<h2 id="heading-characterization-tests-are-temporary-knowledge-infrastructure">Characterization Tests Are Temporary Knowledge Infrastructure</h2>
<p>There's another way I think about these tests.</p>
<p>Legacy systems contain knowledge that's often trapped inside implementation details. A characterization test moves some of that knowledge into an executable form.</p>
<p>Before:</p>
<pre><code class="language-text">Nobody knows what changing this condition will break.
</code></pre>
<p>After:</p>
<pre><code class="language-text">Changing this condition causes these four observable behaviors to change.
</code></pre>
<p>That's already progress.</p>
<p>The test suite becomes part of your understanding of the system. It creates a bridge between:</p>
<pre><code class="language-text">what the code currently does
</code></pre>
<p>and:</p>
<pre><code class="language-text">what we eventually want the system to do
</code></pre>
<p>You don't have to keep every characterization test forever.</p>
<p>Some will become proper specification tests.</p>
<p>Some will disappear when obsolete behavior is intentionally removed.</p>
<p>Some will remain as regression tests.</p>
<p>Their first job is simpler: <strong>make change safer while understanding is still incomplete.</strong></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>AI makes refactoring legacy code faster.</p>
<p>It can explain functions, generate candidate abstractions, extract interfaces, suggest module boundaries, and rewrite large sections of code in seconds.</p>
<p>That makes characterization testing more important, not less.</p>
<p>When the cost of producing a new implementation decreases, the risk shifts toward verifying that the new implementation still preserves the behavior that matters.</p>
<p>Before asking:</p>
<pre><code class="language-text">How should I refactor this?
</code></pre>
<p>ask:</p>
<pre><code class="language-text">What does this do today?
</code></pre>
<p>Then:</p>
<pre><code class="language-text">Which of those behaviors matter?
</code></pre>
<p>Then:</p>
<pre><code class="language-text">How can I prove they still work after the change?
</code></pre>
<p>That is what characterization tests give you.</p>
<p>They don't tell you that legacy behavior is correct. They give you evidence that it exists.</p>
<p>And once that evidence is executable, you can refactor with much more confidence.</p>
<p>The sequence becomes:</p>
<pre><code class="language-text">Understand
↓
Characterize
↓
Refactor
↓
Verify
</code></pre>
<p>AI can accelerate every step in that workflow. But the engineering judgment remains in deciding what behavior deserves to survive, what behavior should change, and when you have enough evidence to safely make that distinction.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Understand a Legacy Codebase Using AI Before Changing it ]]>
                </title>
                <description>
                    <![CDATA[ The first thing many engineers want to do when they inherit a legacy codebase is change it. And I understand the impulse. You open a class that's 1,500 lines long. There are database calls mixed with  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/understand-a-legacy-codebase-with-ai/</link>
                <guid isPermaLink="false">6a888892029633fd14697876</guid>
                
                    <category>
                        <![CDATA[ software architecture ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Artificial Intelligence ]]>
                    </category>
                
                    <category>
                        <![CDATA[ legacy code ]]>
                    </category>
                
                    <category>
                        <![CDATA[ refactoring ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Software Engineering ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Hugo Teijiz ]]>
                </dc:creator>
                <pubDate>Fri, 21 Aug 2026 17:19:14 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/7d94c780-37eb-4bd6-a1e2-da6e25bdfdcb.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>The first thing many engineers want to do when they inherit a legacy codebase is change it. And I understand the impulse.</p>
<p>You open a class that's 1,500 lines long. There are database calls mixed with business rules, configuration values scattered across the repository, methods nobody wants to touch, and comments that refer to systems that disappeared years ago.</p>
<p>Then an AI coding assistant offers to explain the whole thing.</p>
<p>So you ask:</p>
<blockquote>
<p>Refactor this class.</p>
</blockquote>
<p>But that's usually too early.</p>
<p>One of the lessons I've learned from working with legacy systems is that code can be ugly and still contain important knowledge.</p>
<p>A strange condition may encode a business exception. A duplicated calculation may exist because two processes that look identical aren't actually identical. A database column with a terrible name may still be part of an external contract.</p>
<p>And a method nobody understands may be the only thing preventing a production incident that happened eight years ago from happening again.</p>
<p>AI makes it much easier to read unfamiliar software, and that's valuable. But it also makes it much easier to change software before you understand it.</p>
<p>In this tutorial, I'll show you how to use AI for something I believe should happen before refactoring or migration: <strong>codebase archaeology.</strong></p>
<p>You'll learn how to use AI to help you:</p>
<ul>
<li><p>map a repository,</p>
</li>
<li><p>identify entry points,</p>
</li>
<li><p>trace dependencies,</p>
</li>
<li><p>separate business rules from infrastructure,</p>
</li>
<li><p>find hidden side effects,</p>
</li>
<li><p>inspect data flow,</p>
</li>
<li><p>discover implicit contracts,</p>
</li>
<li><p>detect duplicated behavior,</p>
</li>
<li><p>build a dependency map,</p>
</li>
<li><p>identify areas of uncertainty,</p>
</li>
<li><p>and turn those findings into a modernization plan.</p>
</li>
</ul>
<p>The examples use TypeScript, but the process works with most languages and stacks.</p>
<p>The goal isn't to ask AI what the code means and trust the answer. The goal is to use AI to reduce the amount of time you spend looking for the right questions.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>You should be comfortable with:</p>
<ul>
<li><p>reading an existing codebase</p>
</li>
<li><p>TypeScript or a similar object-oriented language</p>
</li>
<li><p>basic software architecture</p>
</li>
<li><p>dependency injection</p>
</li>
<li><p>unit and integration testing</p>
</li>
<li><p>using an AI coding assistant that can inspect repository files</p>
</li>
</ul>
<p>You don't need a specific AI provider, as the workflow matters more than the model.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-why-understanding-has-to-come-before-refactoring">Why Understanding Has to Come Before Refactoring</a></p>
</li>
<li><p><a href="#heading-how-to-start-with-the-repository-not-the-classes">How to Start with the Repository, Not the Classes</a></p>
</li>
<li><p><a href="#heading-how-to-find-the-real-entry-points">How to Find the Real Entry Points</a></p>
</li>
<li><p><a href="#heading-how-to-trace-a-business-capability-through-the-codebase">How to Trace a Business Capability Through the Codebase</a></p>
</li>
<li><p><a href="#heading-how-to-separate-business-rules-from-infrastructure">How to Separate Business Rules from Infrastructure</a></p>
</li>
<li><p><a href="#heading-how-to-find-hidden-side-effects">How to Find Hidden Side Effects</a></p>
</li>
<li><p><a href="#heading-how-to-discover-implicit-contracts">How to Discover Implicit Contracts</a></p>
</li>
<li><p><a href="#heading-how-to-use-ai-to-find-duplicated-business-rules">How to Use AI to Find Duplicated Business Rules</a></p>
</li>
<li><p><a href="#heading-how-to-build-a-lightweight-dependency-map">How to Build a Lightweight Dependency Map</a></p>
</li>
<li><p><a href="#heading-how-to-mark-what-you-still-do-not-understand">How to Mark What You Still Do Not Understand</a></p>
</li>
<li><p><a href="#heading-how-to-validate-ai-findings-against-the-system">How to Validate AI Findings Against the System</a></p>
</li>
<li><p><a href="#heading-how-to-turn-codebase-understanding-into-a-migration-plan">How to Turn Codebase Understanding into a Migration Plan</a></p>
</li>
<li><p><a href="#heading-a-practical-codebase-archaeology-workflow">A Practical Codebase Archaeology Workflow</a></p>
</li>
<li><p><a href="#heading-what-i-would-not-ask-ai-to-do-first">What I Would Not Ask AI to Do First</a></p>
</li>
<li><p><a href="#heading-the-most-useful-ai-output-is-sometimes-a-question">The Most Useful AI Output Is Sometimes a Question</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-why-understanding-has-to-come-before-refactoring">Why Understanding Has to Come Before Refactoring</h2>
<p>Legacy code often creates a false sense of urgency.</p>
<p>You see something obviously coupled or duplicated and immediately want to clean it up.</p>
<p>Consider this function:</p>
<pre><code class="language-typescript">async function approveOrder(order: Order) {
  if (order.total &gt; 10000 &amp;&amp; !order.customer.verified) {
    throw new Error("Manual verification required");
  }

  if (
    order.customer.country === "AR" &amp;&amp;
    order.paymentMethod === "TRANSFER"
  ) {
    order.status = "PENDING";
  } else {
    order.status = "APPROVED";
  }

  await orders.save(order);

  if (order.status === "APPROVED") {
    await billing.createInvoice(order);
  }

  await audit.log({
    action: "ORDER_APPROVAL",
    orderId: order.id,
    status: order.status,
  });

  return order;
}
</code></pre>
<p>At first glance, there are several clear refactoring opportunities:</p>
<ul>
<li><p>You could extract validation.</p>
</li>
<li><p>You could isolate status calculation.</p>
</li>
<li><p>You could move billing behind an interface.</p>
</li>
<li><p>You could create an approval policy.</p>
</li>
</ul>
<p>All of those ideas may be reasonable, but there are questions you should answer first:</p>
<ul>
<li><p>Why is <code>10000</code> important?</p>
</li>
<li><p>Why does an Argentine bank transfer remain pending?</p>
</li>
<li><p>Does invoice creation have to happen after persistence?</p>
</li>
<li><p>Is <code>ORDER_APPROVAL</code> consumed by another system?</p>
</li>
<li><p>Can orders transition from <code>PENDING</code> to <code>APPROVED</code> somewhere else?</p>
</li>
<li><p>Does anything depend on the exact exception message?</p>
</li>
</ul>
<p>You can't answer those questions from syntax alone.</p>
<p>That's where understanding begins.</p>
<p>Instead of asking your AI tool:</p>
<pre><code class="language-text">Refactor this function using clean architecture.
</code></pre>
<p>start with:</p>
<pre><code class="language-text">Analyze this function without changing it.

Identify:

1. explicit business rules,
2. likely business rules that need confirmation,
3. side effects,
4. external dependencies,
5. state transitions,
6. magic values,
7. assumptions that cannot be proven from this file alone.

Do not propose a refactor yet.
</code></pre>
<p>That last line is important: <strong>Do not propose a refactor yet.</strong></p>
<p>You want the model in investigation mode, not solution mode.</p>
<h2 id="heading-how-to-start-with-the-repository-not-the-classes">How to Start with the Repository, Not the Classes</h2>
<p>When I approach an unfamiliar legacy system, I don't start by reading every file. I start by trying to understand the shape of the application.</p>
<p>A repository already contains architectural clues.</p>
<p>Look for directories such as:</p>
<pre><code class="language-text">src/
controllers/
services/
repositories/
models/
jobs/
workers/
scripts/
migrations/
config/
integrations/
tests/
</code></pre>
<p>But don't assume the directory names describe the real architecture.</p>
<p>A directory called <code>services</code> can contain business logic, infrastructure, orchestration, and random utility functions.</p>
<p>A directory called <code>models</code> might contain database entities rather than domain models.</p>
<p>A folder called <code>utils</code> can hide half the application's business logic.</p>
<p>Use the structure as evidence, not truth.</p>
<p>A useful first AI request is:</p>
<pre><code class="language-text">Inspect the repository structure.

Do not analyze individual implementation details yet.

Identify:

- application entry points,
- major modules,
- database technologies,
- external integrations,
- background processing,
- scheduled tasks,
- authentication mechanisms,
- configuration sources,
- tests,
- likely architectural boundaries.

For each conclusion, reference the files or directories
that support it.

Mark anything uncertain explicitly.
</code></pre>
<p>The requirement to reference files matters. Without it, AI can give you a perfectly reasonable architecture that doesn't actually exist.</p>
<p>You want something closer to:</p>
<pre><code class="language-text">HTTP API
Evidence:
- src/server.ts
- src/routes/orders.ts
- src/routes/customers.ts

Background processing
Evidence:
- src/workers/paymentWorker.ts
- src/queues/index.ts

Scheduled jobs
Evidence:
- src/jobs/reconcileInvoices.ts
- src/cron.ts
</code></pre>
<p>Now you have a map you can verify.</p>
<h2 id="heading-how-to-find-the-real-entry-points">How to Find the Real Entry Points</h2>
<p>Web applications often have an obvious HTTP entry point. But legacy systems frequently have several more.</p>
<p>A business operation may begin from:</p>
<ul>
<li><p>an API request,</p>
</li>
<li><p>a scheduled job,</p>
</li>
<li><p>a queue consumer,</p>
</li>
<li><p>a database trigger,</p>
</li>
<li><p>a CLI script,</p>
</li>
<li><p>a file import,</p>
</li>
<li><p>an email handler,</p>
</li>
<li><p>a webhook,</p>
</li>
<li><p>or another application calling the database directly.</p>
</li>
</ul>
<p>If you only analyze controllers, you may miss half the system.</p>
<p>Suppose you search for order creation and find:</p>
<pre><code class="language-text">POST /orders
</code></pre>
<p>It would be easy to assume that all orders enter through that endpoint.</p>
<p>Then you discover:</p>
<pre><code class="language-text">jobs/importMarketplaceOrders.ts
workers/retryFailedOrders.ts
scripts/migratePendingOrders.ts
integrations/shopify/webhook.ts
</code></pre>
<p>Now the same business object has four additional entry paths.</p>
<p>This changes how you think about refactoring.</p>
<p>Ask AI:</p>
<pre><code class="language-text">Find every location that can create, modify,
approve, cancel, or persist an Order.

Include:

- HTTP endpoints,
- background workers,
- scheduled jobs,
- scripts,
- imports,
- webhooks,
- direct repository calls.

Group the results by operation.

For every result, include the file path and
the relevant function or class.
</code></pre>
<p>Then verify those results with repository search.</p>
<p>For example:</p>
<pre><code class="language-bash">rg "orders\.save|orders\.insert|createOrder|approveOrder" src
</code></pre>
<p>AI should accelerate search, not replace it.</p>
<h2 id="heading-how-to-trace-a-business-capability-through-the-codebase">How to Trace a Business Capability Through the Codebase</h2>
<p>Understanding individual files isn't enough.</p>
<p>What usually matters is understanding a <strong>business capability</strong>.</p>
<p>For example:</p>
<blockquote>
<p>Create an order.</p>
</blockquote>
<p>That capability may travel through several layers:</p>
<pre><code class="language-text">HTTP Request
     ↓
Controller
     ↓
Application Service
     ↓
Pricing
     ↓
Inventory
     ↓
Persistence
     ↓
Payment
     ↓
Notification
</code></pre>
<p>The code may not be organized that cleanly, and that's precisely why tracing the capability is useful.</p>
<p>Choose one real workflow and ask:</p>
<pre><code class="language-text">Trace the "Create Order" capability from its entry point
until all observable side effects are complete.

For each step, show:

- file,
- function or class,
- input,
- output,
- state change,
- external call,
- error behavior.

Do not summarize multiple steps into one.
</code></pre>
<p>You want a sequence that you can inspect.</p>
<p>For example:</p>
<pre><code class="language-text">1. POST /orders
   src/routes/orders.ts

2. OrdersController.create()
   src/controllers/OrdersController.ts

3. OrderService.create()
   src/services/OrderService.ts

4. calculatePrice()
   src/services/pricing.ts

5. inventory.reserve()
   src/integrations/inventory.ts

6. ordersRepository.save()
   src/repositories/orders.ts

7. paymentQueue.publish()
   src/queues/payment.ts
</code></pre>
<p>This becomes far more useful than a generic explanation of the architecture.</p>
<p>Now you can ask questions such as:</p>
<ul>
<li><p>Where does the transaction actually begin?</p>
</li>
<li><p>What happens if payment publishing fails?</p>
</li>
<li><p>Is inventory reservation reversible?</p>
</li>
<li><p>Can the order be saved twice?</p>
</li>
<li><p>Which steps are synchronous?</p>
</li>
<li><p>Which failures are retried?</p>
</li>
</ul>
<p>Those are modernization questions.</p>
<h2 id="heading-how-to-separate-business-rules-from-infrastructure">How to Separate Business Rules from Infrastructure</h2>
<p>One of the most useful things you can do during codebase archaeology is identify where business behavior lives.</p>
<p>Legacy applications frequently mix it with infrastructure.</p>
<p>Consider:</p>
<pre><code class="language-typescript">async function saveCustomer(customer: Customer) {
  if (
    customer.type === "ENTERPRISE" &amp;&amp;
    customer.creditLimit &lt; 50000
  ) {
    throw new Error("Invalid enterprise credit limit");
  }

  const connection = await mysql.getConnection();

  await connection.query(
    "INSERT INTO customers (...) VALUES (...)",
    [...]
  );

  await redis.del(`customer:${customer.id}`);

  await eventBus.publish(
    "customer.updated",
    customer
  );
}
</code></pre>
<p>There's at least one business rule:</p>
<pre><code class="language-text">Enterprise customers must have a credit limit &gt;= 50000.
</code></pre>
<p>And several infrastructure concerns:</p>
<pre><code class="language-text">MySQL
Redis
Event bus
</code></pre>
<p>Ask AI to classify the code:</p>
<pre><code class="language-text">Classify each responsibility in this function as one of:

- business rule,
- application orchestration,
- persistence,
- caching,
- messaging,
- logging,
- validation,
- unknown.

Explain why.

Do not move or rewrite any code.
</code></pre>
<p>The <code>unknown</code> category is useful. You don't want the model to force every line into a clean architectural theory.</p>
<p>Some code really is ambiguous until you inspect more context.</p>
<h2 id="heading-how-to-find-hidden-side-effects">How to Find Hidden Side Effects</h2>
<p>Side effects are one of the biggest sources of migration risk.</p>
<p>A function called:</p>
<pre><code class="language-typescript">updateCustomer()
</code></pre>
<p>may do much more than update a customer.</p>
<p>It may:</p>
<ul>
<li><p>write to the database</p>
</li>
<li><p>invalidate cache</p>
</li>
<li><p>emit an event</p>
</li>
<li><p>send an email</p>
</li>
<li><p>update analytics</p>
</li>
<li><p>write an audit record</p>
</li>
<li><p>schedule another job</p>
</li>
</ul>
<p>If you refactor the function and preserve only its return value, you can break production behavior without any compiler error.</p>
<p>A useful investigation prompt is:</p>
<pre><code class="language-text">List every observable side effect produced directly
or indirectly by this function.

For each one, identify:

- the side effect,
- where it happens,
- whether it is synchronous or asynchronous,
- whether failure propagates,
- whether it appears retryable,
- whether it is idempotent,
- whether it can be safely repeated.

Mark uncertain answers as unknown.
</code></pre>
<p>That last property, idempotency, matters a lot.</p>
<p>Suppose a worker does this:</p>
<pre><code class="language-typescript">await chargeCard(order);
await markOrderAsPaid(order);
</code></pre>
<p>If the worker crashes between those two lines and retries, what happens? You may charge the customer twice. And that's not visible from the function name.</p>
<p>Understanding retry semantics is part of understanding the codebase.</p>
<h2 id="heading-how-to-discover-implicit-contracts">How to Discover Implicit Contracts</h2>
<p>Not every contract is declared with an interface. Legacy applications contain many implicit contracts.</p>
<p>For example:</p>
<pre><code class="language-typescript">return {
  status: "ok",
  value: customer.balance.toFixed(2),
};
</code></pre>
<p>Some external consumer may depend on:</p>
<pre><code class="language-json">{
  "status": "ok",
  "value": "100.00"
}
</code></pre>
<p>Changing <code>value</code> from a string to a number can look like an improvement:</p>
<pre><code class="language-json">{
  "status": "ok",
  "value": 100
}
</code></pre>
<p>It can also break a client.</p>
<p>Look for contracts in:</p>
<ul>
<li><p>API responses,</p>
</li>
<li><p>events,</p>
</li>
<li><p>database structures,</p>
</li>
<li><p>CSV exports,</p>
</li>
<li><p>filenames,</p>
</li>
<li><p>environment variables,</p>
</li>
<li><p>error messages,</p>
</li>
<li><p>queue payloads,</p>
</li>
<li><p>and webhook bodies.</p>
</li>
</ul>
<p>Ask:</p>
<pre><code class="language-text">Identify outputs from this module that could be consumed
outside the module.

Include:

- HTTP responses,
- emitted events,
- queue messages,
- files,
- database records,
- exceptions,
- logs used for automated processing.

For each output, explain what evidence suggests that it
may be an external or implicit contract.
</code></pre>
<p>The wording matters:</p>
<blockquote>
<p>what evidence suggests</p>
</blockquote>
<p>not:</p>
<blockquote>
<p>tell me which contracts exist</p>
</blockquote>
<p>because you may not be able to prove the consumer from the current repository.</p>
<h2 id="heading-how-to-use-ai-to-find-duplicated-business-rules">How to Use AI to Find Duplicated Business Rules</h2>
<p>Duplicated code is easy to detect. Duplicated <strong>business meaning</strong> is harder.</p>
<p>You may find:</p>
<pre><code class="language-typescript">if (customer.type === "PREMIUM") {
  discount = total * 0.1;
}
</code></pre>
<p>in one module.</p>
<p>And elsewhere:</p>
<pre><code class="language-typescript">if (account.plan === "GOLD") {
  price = price * 0.9;
}
</code></pre>
<p>Those might represent the same business rule, or they might not.</p>
<p>AI is useful for identifying candidates.</p>
<p>Ask:</p>
<pre><code class="language-text">Search the repository for business rules related to
customer discounts.

Group implementations that appear semantically related,
even if variable names differ.

For each group:

- list file locations,
- describe the apparent rule,
- highlight differences,
- do not assume the rules should be unified.
</code></pre>
<p>That final instruction is important.</p>
<p>Duplication is sometimes accidental.</p>
<p>Sometimes it represents two domains that evolved independently.</p>
<p>Don't let an AI assistant turn:</p>
<pre><code class="language-text">similar
</code></pre>
<p>into:</p>
<pre><code class="language-text">must be merged
</code></pre>
<p>without evidence.</p>
<h2 id="heading-how-to-build-a-lightweight-dependency-map">How to Build a Lightweight Dependency Map</h2>
<p>At some point, you need to understand which parts of the system depend on which others.</p>
<p>You don't need a perfect enterprise architecture diagram. A lightweight dependency map is enough to start.</p>
<p>For example:</p>
<pre><code class="language-text">Orders
 ├── Customers
 ├── Inventory
 ├── Payments
 ├── Notifications
 └── Database

Payments
 ├── Payment Provider
 ├── Audit
 └── Database
</code></pre>
<p>Ask AI to extract module-level dependencies:</p>
<pre><code class="language-text">Build a module dependency map from the repository.

Only include dependencies supported by imports,
constructor dependencies, explicit calls, or configuration.

Output:

Module A -&gt; Module B

For each dependency, provide at least one source file
that demonstrates it.

Do not infer dependencies from names alone.
</code></pre>
<p>You can then compare the result with automated tools.</p>
<p>For JavaScript or TypeScript projects, dependency analysis tools can help you find:</p>
<ul>
<li><p>circular dependencies</p>
</li>
<li><p>cross-module imports</p>
</li>
<li><p>high fan-in</p>
</li>
<li><p>high fan-out</p>
</li>
</ul>
<p>AI is useful for explaining why those dependencies may matter. Static analysis is better at proving that they exist.</p>
<p>Use both.</p>
<h2 id="heading-how-to-mark-what-you-still-do-not-understand">How to Mark What You Still Do Not Understand</h2>
<p>This is one of the most important parts of the process.</p>
<p>A useful system map doesn't only contain answers. It also contains uncertainty.</p>
<p>I like keeping an explicit list such as:</p>
<pre><code class="language-markdown">## Open Questions

- Why is the enterprise credit threshold 50,000?
- Is `ORDER_APPROVAL` consumed outside this repository?
- Can marketplace orders bypass inventory validation?
- Is `customer.balance` allowed to be negative?
- What process transitions PENDING orders to APPROVED?
- Is `legacy_customer_id` still used by another system?
</code></pre>
<p>You can ask AI to generate this list:</p>
<pre><code class="language-text">Based on everything analyzed so far, list the questions
that can't be answered safely from the repository.

Focus on questions that would matter during:

- refactoring,
- migration,
- schema changes,
- interface changes,
- removal of code.

Do not answer the questions.
</code></pre>
<p>I like this prompt because it does the opposite of what we normally ask AI to do. It asks the model to identify where it should <strong>not</strong> pretend to know.</p>
<p>A modernization plan should include those unknowns.</p>
<h2 id="heading-how-to-validate-ai-findings-against-the-system">How to Validate AI Findings Against the System</h2>
<p>AI-generated explanations can sound convincing even when they're incomplete. So every important finding should have another source of evidence.</p>
<p>I use a simple hierarchy.</p>
<h3 id="heading-repository-search">Repository Search</h3>
<p>If AI says a function is called only once, search for it.</p>
<pre><code class="language-bash">rg "approveOrder" .
</code></pre>
<h3 id="heading-tests">Tests</h3>
<p>Tests often reveal assumptions that implementation code doesn't explain.</p>
<p>Look for:</p>
<pre><code class="language-text">expected errors
special values
boundary cases
fixture data
historical behavior
</code></pre>
<h3 id="heading-database-schema">Database Schema</h3>
<p>The schema may reveal key things like:</p>
<ul>
<li><p>nullable fields</p>
</li>
<li><p>foreign keys</p>
</li>
<li><p>defaults</p>
</li>
<li><p>legacy columns</p>
</li>
<li><p>constraints</p>
</li>
<li><p>status values</p>
</li>
</ul>
<h3 id="heading-logs-and-observability">Logs and Observability</h3>
<p>Production telemetry can tell you whether a supposedly unused path is still active.</p>
<h3 id="heading-version-history">Version History</h3>
<p>Git history can sometimes answer questions that source code can't.</p>
<p>For example:</p>
<pre><code class="language-bash">git log -S "Manual verification required" --all
</code></pre>
<p>or:</p>
<pre><code class="language-bash">git blame src/orders/approveOrder.ts
</code></pre>
<p>The commit that introduced a strange condition may contain the explanation.</p>
<p>This is an area where AI can help summarize history:</p>
<pre><code class="language-text">Review the commits that changed this function.

Build a timeline of behavior changes.

For each change, include:

- commit,
- date,
- behavior changed,
- stated reason if available.

Do not infer a reason if the commit history does not provide one.
</code></pre>
<p>That can save a surprising amount of time.</p>
<h2 id="heading-how-to-turn-codebase-understanding-into-a-migration-plan">How to Turn Codebase Understanding into a Migration Plan</h2>
<p>Once you understand one capability, you can begin making decisions. But not before.</p>
<p>Suppose your investigation produces this:</p>
<pre><code class="language-text">Create Order

Business rules:
- active customer required
- premium customers receive 10% discount
- inventory must be available

Side effects:
- order persisted
- inventory reserved
- payment queued
- confirmation email sent

External contracts:
- POST /orders response
- payment queue payload
- order.created event

Unknowns:
- retry semantics for inventory reservation
- whether event consumers require exact field names
</code></pre>
<p>Now you can decide what to protect.</p>
<p>For example:</p>
<pre><code class="language-text">Protect first:
- pricing behavior
- API response
- payment payload
- event schema
</code></pre>
<p>Then decide what can be refactored.</p>
<pre><code class="language-text">Candidate boundaries:
- pricing policy
- inventory gateway
- payment publisher
- notification service
</code></pre>
<p>Then decide what needs investigation.</p>
<pre><code class="language-text">Block migration until understood:
- inventory retry behavior
- event consumers
</code></pre>
<p>That's already a migration plan.</p>
<p>Notice what AI did not do: it didn't decide the target architecture.</p>
<p>It helped make the current architecture observable enough for you to make that decision.</p>
<h2 id="heading-a-practical-codebase-archaeology-workflow">A Practical Codebase Archaeology Workflow</h2>
<p>If I had to reduce this process to something repeatable, I would use these steps.</p>
<h3 id="heading-1-map-the-repository">1. Map the Repository</h3>
<p>Identify:</p>
<ul>
<li><p>entry points</p>
</li>
<li><p>modules</p>
</li>
<li><p>persistence</p>
</li>
<li><p>integrations</p>
</li>
<li><p>workers</p>
</li>
<li><p>jobs</p>
</li>
<li><p>tests</p>
</li>
<li><p>configuration</p>
</li>
</ul>
<p>Don't refactor anything.</p>
<h3 id="heading-2-choose-one-capability">2. Choose One Capability</h3>
<p>Pick something concrete:</p>
<pre><code class="language-text">Create Order
Approve Loan
Generate Invoice
Register Customer
Cancel Subscription
</code></pre>
<p>Avoid trying to understand the whole product at once.</p>
<h3 id="heading-3-trace-it-end-to-end">3. Trace It End to End</h3>
<p>Follow:</p>
<pre><code class="language-text">input
↓
business logic
↓
state changes
↓
external calls
↓
output
</code></pre>
<p>Record every file involved.</p>
<h3 id="heading-4-extract-business-rules">4. Extract Business Rules</h3>
<p>Separate:</p>
<ul>
<li><p>explicit rules</p>
</li>
<li><p>likely rules</p>
</li>
<li><p>infrastructure behavior</p>
</li>
<li><p>unknowns</p>
</li>
</ul>
<h3 id="heading-5-identify-side-effects">5. Identify Side Effects</h3>
<p>Find:</p>
<ul>
<li><p>writes</p>
</li>
<li><p>messages</p>
</li>
<li><p>emails</p>
</li>
<li><p>jobs</p>
</li>
<li><p>cache changes</p>
</li>
<li><p>external calls</p>
</li>
</ul>
<h3 id="heading-6-discover-contracts">6. Discover Contracts</h3>
<p>Look for:</p>
<ul>
<li><p>APIs</p>
</li>
<li><p>event schemas</p>
</li>
<li><p>database assumptions</p>
</li>
<li><p>exported files</p>
</li>
<li><p>error behavior</p>
</li>
</ul>
<h3 id="heading-7-map-dependencies">7. Map Dependencies</h3>
<p>Document:</p>
<pre><code class="language-text">module -&gt; module
</code></pre>
<p>and identify coupling.</p>
<h3 id="heading-8-record-unknowns">8. Record Unknowns</h3>
<p>Don't hide uncertainty. Create an explicit list.</p>
<h3 id="heading-9-verify">9. Verify</h3>
<p>Use:</p>
<ul>
<li><p>repository search</p>
</li>
<li><p>tests</p>
</li>
<li><p>schema</p>
</li>
<li><p>logs</p>
</li>
<li><p>Git history</p>
</li>
<li><p>production telemetry</p>
</li>
</ul>
<h3 id="heading-10-only-then-plan-the-change">10. Only Then Plan the Change</h3>
<p>Decide:</p>
<ul>
<li><p>what behavior must survive,</p>
</li>
<li><p>what code can disappear,</p>
</li>
<li><p>what boundaries should be introduced,</p>
</li>
<li><p>what needs tests,</p>
</li>
<li><p>and what can migrate first.</p>
</li>
</ul>
<h2 id="heading-what-i-would-not-ask-ai-to-do-first">What I Would Not Ask AI to Do First</h2>
<p>There are several prompts I avoid at the beginning of a legacy modernization project.</p>
<p>For example:</p>
<pre><code class="language-text">Rewrite this application using Clean Architecture.
</code></pre>
<p>or:</p>
<pre><code class="language-text">Convert this monolith into microservices.
</code></pre>
<p>or:</p>
<pre><code class="language-text">Modernize this entire repository.
</code></pre>
<p>or even:</p>
<pre><code class="language-text">Find all the bad code.
</code></pre>
<p>The problem isn't that AI can't produce useful output from those prompts. It can.</p>
<p>The problem is that those questions already contain a solution.</p>
<p>You're asking for:</p>
<pre><code class="language-text">Clean Architecture
Microservices
Rewrite
Bad code
</code></pre>
<p>before you've established what the system actually needs.</p>
<p>A better sequence is:</p>
<pre><code class="language-text">What exists?
↓
Why does it exist?
↓
What behavior matters?
↓
What is uncertain?
↓
What should change?
</code></pre>
<p>That sequence is slower for the first hour, but it's usually much faster for the rest of the project.</p>
<h2 id="heading-the-most-useful-ai-output-is-sometimes-a-question">The Most Useful AI Output Is Sometimes a Question</h2>
<p>There's a tendency to evaluate AI coding tools by how much code they generate.</p>
<p>For legacy systems, I think that misses part of their value.</p>
<p>One of the most useful outputs can be:</p>
<blockquote>
<p>I cannot determine why this condition exists from the available code.</p>
</blockquote>
<p>Or:</p>
<blockquote>
<p>This event appears to have no consumer in the current repository, but external consumers cannot be ruled out.</p>
</blockquote>
<p>Or:</p>
<blockquote>
<p>These two discount calculations look similar, but their behavior differs for zero-value orders.</p>
</blockquote>
<p>Those are useful findings that tell an engineer where to investigate.</p>
<p>A confident but incorrect answer is much more dangerous.</p>
<p>When working with legacy systems, uncertainty is information. Treat it that way.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>AI makes unfamiliar codebases much easier to explore.</p>
<p>You can use it to summarize modules, trace execution paths, extract candidate business rules, find side effects, compare implementations, analyze Git history, and build dependency maps.</p>
<p>That can remove a large amount of mechanical investigation work.</p>
<p>But understanding a system isn't the same as generating an explanation of it. Legacy applications contain context that may exist outside the source code:</p>
<ul>
<li><p>production behavior,</p>
</li>
<li><p>old incidents,</p>
</li>
<li><p>external consumers,</p>
</li>
<li><p>business exceptions,</p>
</li>
<li><p>undocumented integrations,</p>
</li>
<li><p>and organizational history.</p>
</li>
</ul>
<p>AI can help you find evidence. It can't manufacture missing history.</p>
<p>That's why I prefer to use it as an investigator before I use it as a transformer.</p>
<p>Start with:</p>
<pre><code class="language-text">What does this system actually do?
</code></pre>
<p>Then ask:</p>
<pre><code class="language-text">What do I still not understand?
</code></pre>
<p>Only after that should you ask:</p>
<pre><code class="language-text">What should I change?
</code></pre>
<p>The faster AI lets you modify software, the more important that sequence becomes.</p>
<p>Because changing code you understand is engineering. But changing code you don't understand is experimentation.</p>
<p>And production is usually the most expensive place to run that experiment.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Modernize a Legacy Application with AI Without Turning It Into a Rewrite ]]>
                </title>
                <description>
                    <![CDATA[ I have seen legacy migrations considered successful because the old framework disappeared from the repository. Six months later, the team was still dealing with the same coupling, the same unclear bus ]]>
                </description>
                <link>https://www.freecodecamp.org/news/modernize-legacy-applications-with-ai/</link>
                <guid isPermaLink="false">6a7e4a380ee61c58fa48acb3</guid>
                
                    <category>
                        <![CDATA[ software architecture ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Artificial Intelligence ]]>
                    </category>
                
                    <category>
                        <![CDATA[ refactoring ]]>
                    </category>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Software Engineering ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Hugo Teijiz ]]>
                </dc:creator>
                <pubDate>Thu, 13 Aug 2026 22:50:32 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/2eb7ca1a-00d1-4dd4-a4a0-2f64eeb40388.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>I have seen legacy migrations considered successful because the old framework disappeared from the repository.</p>
<p>Six months later, the team was still dealing with the same coupling, the same unclear business rules, and almost the same deployment problems.</p>
<p>The technology had changed but the system hadn't changed very much as a whole.</p>
<p>AI makes this problem even more interesting.</p>
<p>It can translate code faster than a team could do manually. It can explain unfamiliar classes, generate tests, create adapters, update APIs, and remove a significant amount of repetitive work.</p>
<p>But if you point an AI coding tool at an old application and simply ask it to migrate everything to a modern stack, there's a good chance you'll get exactly what you asked for: <strong>the same system, rewritten faster.</strong></p>
<p>That's not necessarily modernization.</p>
<p>In this tutorial, I want to show you a different way to use AI during a legacy migration.</p>
<p>Instead of treating AI as an automated code translator, you'll use it to help you:</p>
<ul>
<li><p>understand an unfamiliar codebase,</p>
</li>
<li><p>identify business rules and hidden dependencies,</p>
</li>
<li><p>build a behavioral safety net,</p>
</li>
<li><p>find boundaries for incremental migration,</p>
</li>
<li><p>refactor before replacing,</p>
</li>
<li><p>automate repetitive transformations,</p>
</li>
<li><p>compare old and new behavior,</p>
</li>
<li><p>and detect regressions before they reach production.</p>
</li>
</ul>
<p>The examples use TypeScript, but the process itself isn't tied to TypeScript or Node.js.</p>
<p>The important part is the workflow. AI can make migration work faster. But Engineering still has to decide what's worth migrating.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>You should be comfortable with:</p>
<ul>
<li><p>basic TypeScript,</p>
</li>
<li><p>unit and integration testing,</p>
</li>
<li><p>dependency injection,</p>
</li>
<li><p>software architecture concepts,</p>
</li>
<li><p>and working with an existing codebase.</p>
</li>
</ul>
<p>The examples use Vitest, but the same ideas apply if you use Jest or another testing framework.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-how-to-avoid-a-one-to-one-legacy-migration">How to Avoid a One-to-One Legacy Migration</a></p>
</li>
<li><p><a href="#heading-how-to-map-a-legacy-codebase-before-changing-it">How to Map a Legacy Codebase Before Changing It</a></p>
</li>
<li><p><a href="#heading-how-to-build-characterization-tests-before-refactoring">How to Build Characterization Tests Before Refactoring</a></p>
</li>
<li><p><a href="#heading-how-to-find-safe-migration-seams">How to Find Safe Migration Seams</a></p>
</li>
<li><p><a href="#heading-how-to-refactor-toward-explicit-responsibilities">How to Refactor Toward Explicit Responsibilities</a></p>
</li>
<li><p><a href="#heading-how-to-use-ai-for-mechanical-transformations">How to Use AI for Mechanical Transformations</a></p>
</li>
<li><p><a href="#heading-how-to-migrate-in-small-vertical-slices">How to Migrate in Small Vertical Slices</a></p>
</li>
<li><p><a href="#heading-how-to-compare-legacy-and-modern-behavior">How to Compare Legacy and Modern Behavior</a></p>
</li>
<li><p><a href="#heading-how-to-use-shadow-traffic-to-find-regressions">How to Use Shadow Traffic to Find Regressions</a></p>
</li>
<li><p><a href="#heading-how-to-test-the-architecture-you-actually-want">How to Test the Architecture You Actually Want</a></p>
</li>
<li><p><a href="#heading-how-to-decide-which-tasks-ai-should-handle">How to Decide Which Tasks AI Should Handle</a></p>
</li>
<li><p><a href="#heading-how-to-measure-whether-the-migration-actually-improved-the-system">How to Measure Whether the Migration Actually Improved the System</a></p>
</li>
<li><p><a href="#heading-the-risk-i-worry-about-most-with-ai-assisted-migration">The Risk I Worry About Most with AI-Assisted Migration</a></p>
</li>
<li><p><a href="#heading-a-practical-migration-workflow">A Practical Migration Workflow</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-how-to-avoid-a-one-to-one-legacy-migration">How to Avoid a One-to-One Legacy Migration</h2>
<p>Imagine that you find this function in an old order-processing system:</p>
<pre><code class="language-typescript">async function processOrder(order: Order) {
  if (!order.customer.active) {
    throw new Error("Inactive customer");
  }

  const discount =
    order.customer.type === "PREMIUM"
      ? order.total * 0.1
      : 0;

  const finalAmount = order.total - discount;

  await db.orders.insert({
    customerId: order.customer.id,
    amount: finalAmount,
  });

  await paymentGateway.charge(
    order.customer.card,
    finalAmount,
  );

  await mailer.send(
    order.customer.email,
    "Order processed",
  );

  return finalAmount;
}
</code></pre>
<p>The function works, but it also does quite a lot.</p>
<p>It validates the customer, applies a pricing rule, persists data, charges a payment method, and sends a notification.</p>
<p>A one-to-one migration might turn this into a prettier TypeScript service with newer libraries while keeping all those responsibilities together.</p>
<p>You might replace an old controller with a new controller, an old service with a new service, and an old ORM with a new ORM...and still preserve the same architectural problem.</p>
<p>This is one of the first places where AI can work against you.</p>
<p>If your prompt is:</p>
<pre><code class="language-text">Convert this legacy class to TypeScript.
</code></pre>
<p>the model will normally preserve the structure because preserving the structure is the task you gave it.</p>
<p>Before asking AI to transform code, separate two questions:</p>
<ol>
<li><p><strong>What behavior must survive?</strong></p>
</li>
<li><p><strong>What design should survive?</strong></p>
</li>
</ol>
<p>Those aren't the same question.</p>
<p>Sometimes an implementation is old but its behavior is still essential. Sometimes the behavior matters but the implementation should disappear. And sometimes you discover that neither needs to survive.</p>
<p>That distinction should happen before the bulk migration begins.</p>
<h2 id="heading-how-to-map-a-legacy-codebase-before-changing-it">How to Map a Legacy Codebase Before Changing It</h2>
<p>The first difficult part of a legacy migration is usually understanding what you actually have.</p>
<p>Documentation helps when it exists. But in many systems, the real documentation is distributed across:</p>
<ul>
<li><p>conditional statements,</p>
</li>
<li><p>database constraints,</p>
</li>
<li><p>scheduled jobs,</p>
</li>
<li><p>comments,</p>
</li>
<li><p>logs,</p>
</li>
<li><p>integration code,</p>
</li>
<li><p>tests,</p>
</li>
<li><p>configuration,</p>
</li>
<li><p>and knowledge that lives in people's heads.</p>
</li>
</ul>
<p>This is an area where AI can save time without being asked to make architectural decisions.</p>
<p>Take the previous <code>processOrder</code> function. Instead of asking AI to rewrite it, start with questions such as:</p>
<pre><code class="language-text">Identify the business rules in this function.

List every side effect.

Which external systems does it depend on?

Which parts could be expressed as pure functions?

Which observable behaviors should probably be protected
with tests before this function is changed?

Do not rewrite the function.
</code></pre>
<p>The last instruction matters more than it may seem.</p>
<p>When analysis and transformation happen in the same request, it becomes easy for an AI tool to solve a design problem you haven't fully understood yet.</p>
<p>I prefer to make the analysis explicit first.</p>
<p>For a larger codebase, repeat the process at several levels.</p>
<p>At repository level, look for:</p>
<ul>
<li><p>entry points,</p>
</li>
<li><p>database access,</p>
</li>
<li><p>external APIs,</p>
</li>
<li><p>message queues,</p>
</li>
<li><p>background jobs,</p>
</li>
<li><p>scheduled tasks,</p>
</li>
<li><p>configuration,</p>
</li>
<li><p>shared state,</p>
</li>
<li><p>authentication,</p>
</li>
<li><p>and authorization.</p>
</li>
</ul>
<p>At module level, look for:</p>
<ul>
<li><p>business rules,</p>
</li>
<li><p>dependencies,</p>
</li>
<li><p>side effects,</p>
</li>
<li><p>duplicated logic,</p>
</li>
<li><p>highly coupled classes,</p>
</li>
<li><p>and implicit contracts.</p>
</li>
</ul>
<p>At function level, look for:</p>
<ul>
<li><p>inputs,</p>
</li>
<li><p>outputs,</p>
</li>
<li><p>exceptions,</p>
</li>
<li><p>state changes,</p>
</li>
<li><p>external calls,</p>
</li>
<li><p>and edge cases.</p>
</li>
</ul>
<p>AI can make this exploration much faster. But its findings should be checked against the actual repository, tests, schema, logs, and production behavior.</p>
<p>A confident explanation of the code is still only an explanation. <strong>The repository remains the source of truth.</strong></p>
<h2 id="heading-how-to-build-characterization-tests-before-refactoring">How to Build Characterization Tests Before Refactoring</h2>
<p>One of the uncomfortable parts of legacy software is that strange behavior is not necessarily accidental.</p>
<p>You may find code that looks obviously wrong and discover later that another part of the business depends on it.</p>
<p>This is where characterization tests are useful.</p>
<p>Michael Feathers discusses this approach in <a href="https://www.pearson.com/en-us/subject-catalog/p/working-effectively-with-legacy-code/P200000008984/9780131177055"><em>Working Effectively with Legacy Code</em></a>: instead of beginning by describing how the system should behave, you first capture how it behaves today.</p>
<p>Consider this function:</p>
<pre><code class="language-typescript">export function calculateDiscount(
  customerType: string,
  total: number,
): number {
  if (customerType === "PREMIUM") {
    return total * 0.1;
  }

  return 0;
}
</code></pre>
<p>You can protect its current behavior with tests:</p>
<pre><code class="language-typescript">import { describe, expect, it } from "vitest";
import { calculateDiscount } from "./calculateDiscount";

describe("calculateDiscount", () =&gt; {
  it("applies a 10 percent discount to premium customers", () =&gt; {
    expect(
      calculateDiscount("PREMIUM", 100),
    ).toBe(10);
  });

  it("does not discount regular customers", () =&gt; {
    expect(
      calculateDiscount("REGULAR", 100),
    ).toBe(0);
  });

  it("returns zero when the order total is zero", () =&gt; {
    expect(
      calculateDiscount("PREMIUM", 0),
    ).toBe(0);
  });
});
</code></pre>
<p>AI is useful for expanding this safety net.</p>
<p>For example:</p>
<pre><code class="language-text">Generate characterization tests for this function.

Preserve the existing behavior.

Include:
- normal inputs,
- boundary values,
- invalid inputs,
- exceptions,
- observable side effects.

Do not redesign the function.
</code></pre>
<p>Then review what it generates.</p>
<p>You aren't proving that the old behavior is correct. You're recording what will change if you refactor it.</p>
<p>That difference matters.</p>
<p>If a test captures a behavior you later decide is a bug, change it intentionally. What you want to avoid is changing behavior accidentally and discovering the difference after deployment.</p>
<h2 id="heading-how-to-find-safe-migration-seams">How to Find Safe Migration Seams</h2>
<p>Legacy applications rarely need to be replaced all at once.</p>
<p>They usually need places where the old and new systems can coexist temporarily.</p>
<p>Feathers also describes the idea of a <strong>seam</strong> in <em>Working Effectively with Legacy Code</em>: a place where you can alter behavior without having to modify everything around it.</p>
<p>The order-processing example gives you one possible seam.</p>
<p>The original function contains:</p>
<ul>
<li><p>customer validation,</p>
</li>
<li><p>discount calculation,</p>
</li>
<li><p>database persistence,</p>
</li>
<li><p>payment processing,</p>
</li>
<li><p>and email notification.</p>
</li>
</ul>
<p>The first two belong naturally to business behavior. The others involve infrastructure. That suggests a possible boundary.</p>
<p><strong>Domain/application responsibilities:</strong></p>
<ul>
<li><p>customer rules,</p>
</li>
<li><p>pricing rules,</p>
</li>
<li><p>order workflow.</p>
</li>
</ul>
<p><strong>Infrastructure responsibilities:</strong></p>
<ul>
<li><p>database,</p>
</li>
<li><p>payment provider,</p>
</li>
<li><p>email provider.</p>
</li>
</ul>
<p>AI can help identify candidates for these boundaries.</p>
<p>For example:</p>
<pre><code class="language-text">Analyze these files and identify:

- business rules,
- infrastructure concerns,
- side effects,
- shared mutable state,
- duplicated logic,
- dependencies that make isolated testing difficult.

Suggest possible boundaries.

Do not rewrite the code yet.
</code></pre>
<p>Again, the AI output is input to an engineering decision. It shouldn't become the decision automatically.</p>
<p>When you find a good seam, you gain a place where modernization can progress without requiring a rewrite of the entire application.</p>
<h2 id="heading-how-to-refactor-toward-explicit-responsibilities">How to Refactor Toward Explicit Responsibilities</h2>
<p>Once you understand a section of the code and have tests around its current behavior, refactoring becomes less dangerous.</p>
<p>The pricing rule can become a pure function:</p>
<pre><code class="language-typescript">export function calculateDiscount(
  customerType: string,
  total: number,
): number {
  if (customerType === "PREMIUM") {
    return total * 0.1;
  }

  return 0;
}
</code></pre>
<p>Customer validation can be separated:</p>
<pre><code class="language-typescript">export function validateCustomer(
  customer: Customer,
): void {
  if (!customer.active) {
    throw new Error("Inactive customer");
  }
}
</code></pre>
<p>Infrastructure can move behind contracts:</p>
<pre><code class="language-typescript">export interface OrderRepository {
  save(order: PersistedOrder): Promise&lt;void&gt;;
}

export interface PaymentGateway {
  charge(
    card: string,
    amount: number,
  ): Promise&lt;void&gt;;
}

export interface NotificationService {
  sendOrderConfirmation(
    email: string,
  ): Promise&lt;void&gt;;
}
</code></pre>
<p>The application workflow becomes easier to read:</p>
<pre><code class="language-typescript">export class ProcessOrder {
  constructor(
    private readonly orders: OrderRepository,
    private readonly payments: PaymentGateway,
    private readonly notifications: NotificationService,
  ) {}

  async execute(order: Order): Promise&lt;number&gt; {
    validateCustomer(order.customer);

    const discount = calculateDiscount(
      order.customer.type,
      order.total,
    );

    const finalAmount =
      order.total - discount;

    await this.orders.save({
      customerId: order.customer.id,
      amount: finalAmount,
    });

    await this.payments.charge(
      order.customer.card,
      finalAmount,
    );

    await this.notifications.sendOrderConfirmation(
      order.customer.email,
    );

    return finalAmount;
  }
}
</code></pre>
<p>There's nothing particularly revolutionary in this refactoring. That's part of the point.</p>
<p>Modernization doesn't require an exotic architecture.</p>
<p>Often the important improvement is simply making responsibilities explicit enough that the next change doesn't require understanding the entire application.</p>
<h2 id="heading-how-to-use-ai-for-mechanical-transformations">How to Use AI for Mechanical Transformations</h2>
<p>Once the boundaries are clear, AI becomes much more useful for implementation.</p>
<p>A surprising amount of migration work is necessary but repetitive:</p>
<ul>
<li><p>translating APIs,</p>
</li>
<li><p>replacing framework conventions,</p>
</li>
<li><p>generating adapters,</p>
</li>
<li><p>converting configuration,</p>
</li>
<li><p>updating type definitions,</p>
</li>
<li><p>changing data access libraries,</p>
</li>
<li><p>and updating repetitive integration code.</p>
</li>
</ul>
<p>These are good places to use AI.</p>
<p>Imagine that the old system performs SQL directly:</p>
<pre><code class="language-typescript">async function getCustomer(id: number) {
  const result = await db.query(
    `SELECT * FROM customer WHERE id = ${id}`,
  );

  return result[0];
}
</code></pre>
<p>Before generating the new implementation, define the contract you want:</p>
<pre><code class="language-typescript">export interface CustomerRepository {
  findById(id: number): Promise&lt;Customer | null&gt;;
}
</code></pre>
<p>Then constrain the transformation:</p>
<pre><code class="language-text">Implement CustomerRepository using the new database client.

Constraints:

- Keep the CustomerRepository interface unchanged.
- Use parameterized queries.
- Do not move business rules into the repository.
- Preserve the existing null behavior.
- Preserve the existing error semantics.
- Return only the implementation.
</code></pre>
<p>This is a very different request from:</p>
<pre><code class="language-text">Modernize this database code.
</code></pre>
<p>In the first case, you made the architectural decision and asked AI to implement within that boundary.</p>
<p>That is where I find AI most useful in migration work. It removes mechanical effort after the important decisions have already been made.</p>
<h2 id="heading-how-to-migrate-in-small-vertical-slices">How to Migrate in Small Vertical Slices</h2>
<p>Large migrations become difficult to reason about when thousands of files change together.</p>
<p>A safer unit of change is often a business capability.</p>
<p>Instead of migrating all controllers, then all services, and finally all repositories, migrate one complete capability.</p>
<p>For example:</p>
<p><strong>Create Order</strong></p>
<ul>
<li><p>API</p>
</li>
<li><p>application logic</p>
</li>
<li><p>domain rules</p>
</li>
<li><p>persistence</p>
</li>
<li><p>tests</p>
</li>
</ul>
<p>Then move to the next capability.</p>
<p>This has several advantages. First, the migration remains closer to deployable software. Second, the context you give an AI tool stays smaller.</p>
<p>Testing also becomes more focused. And if something goes wrong, the failure is easier to isolate.</p>
<p>A useful first prompt for a vertical slice is analysis-only:</p>
<pre><code class="language-text">We are migrating the Create Order capability.

The legacy implementation is under /legacy/orders.

The target architecture separates:
- domain,
- application,
- infrastructure.

The characterization tests under /tests/legacy
describe behavior that must remain compatible.

Analyze the current implementation.

List:
1. business rules,
2. external dependencies,
3. side effects,
4. likely migration risks,
5. files that need to change.

Do not generate code yet.
</code></pre>
<p>Review that output, then plan the actual transformation.</p>
<p>This is also compatible with an incremental replacement strategy such as Martin Fowler's <a href="https://martinfowler.com/bliki/StranglerFigApplication.html">Strangler Fig</a> approach, where new functionality gradually takes over from an older system instead of requiring one large cutover.</p>
<p>The important word is <strong>gradually</strong>.</p>
<p>AI can increase transformation speed. That doesn't make a big-bang migration less risky.</p>
<h2 id="heading-how-to-compare-legacy-and-modern-behavior">How to Compare Legacy and Modern Behavior</h2>
<p>Unit tests give you one kind of safety.</p>
<p>For a migration, I also like comparing the old and new implementations directly.</p>
<p>Suppose both systems can process the same order. You can run the same fixture through each one:</p>
<pre><code class="language-typescript">const inputs = [
  premiumCustomerOrder,
  standardCustomerOrder,
  inactiveCustomerOrder,
];

for (const input of inputs) {
  const legacyResult =
    await legacyProcessor(input);

  const modernResult =
    await modernProcessor(input);

  expect(modernResult).toEqual(legacyResult);
}
</code></pre>
<p>This is a simple form of differential testing. You can do the same thing at the HTTP boundary.</p>
<p>Send the same <code>POST /orders</code> request to both versions and compare:</p>
<ul>
<li><p>status codes</p>
</li>
<li><p>response payloads</p>
</li>
<li><p>database changes</p>
</li>
<li><p>emitted events</p>
</li>
<li><p>external calls</p>
</li>
<li><p>errors</p>
</li>
</ul>
<p>An important point: a difference isn't automatically a bug. Sometimes behavior is supposed to change. The useful thing is making the difference visible so someone has to classify it deliberately.</p>
<p>AI can help here too.</p>
<p>If you have hundreds of mismatches, you can ask it to group them:</p>
<pre><code class="language-text">Analyze these behavioral mismatches.

Group them by likely cause.

Pay particular attention to:
- rounding,
- null handling,
- timezone conversion,
- validation,
- serialization,
- data mapping.

Do not label a mismatch as a defect unless the
available evidence supports that conclusion.
</code></pre>
<p>This is a good use of AI because the model is reducing investigation work. It's not deciding whether production behavior is acceptable.</p>
<h2 id="heading-how-to-use-shadow-traffic-to-find-regressions">How to Use Shadow Traffic to Find Regressions</h2>
<p>Eventually, test fixtures stop being representative enough.</p>
<p>Production systems receive combinations of inputs nobody thought to put into a test suite.</p>
<p>One way to observe those differences is shadow traffic. The legacy application continues serving the user's request, and a copy of that request also goes to the new implementation. The new result is used for comparison only and isn't returned to the user.</p>
<p>For example:</p>
<pre><code class="language-text">Legacy:
200
{ "total": 90 }

Modern:
200
{ "total": 90 }

MATCH
</code></pre>
<p>Or:</p>
<pre><code class="language-text">Legacy:
200
{ "total": 90 }

Modern:
200
{ "total": 100 }

MISMATCH
</code></pre>
<p>Collecting those mismatches gives you evidence about how the new system behaves under real traffic without immediately exposing users to it.</p>
<p>This technique comes with operational considerations. You need to think carefully about:</p>
<ul>
<li><p>duplicated side effects,</p>
</li>
<li><p>payment calls,</p>
</li>
<li><p>emails,</p>
</li>
<li><p>writes,</p>
</li>
<li><p>privacy,</p>
</li>
<li><p>production load,</p>
</li>
<li><p>and external API usage.</p>
</li>
</ul>
<p>A shadow instance should generally avoid performing irreversible side effects.</p>
<p>For example, replace the real payment adapter with a recording adapter:</p>
<pre><code class="language-typescript">export class RecordingPaymentGateway
  implements PaymentGateway {

  public readonly calls: Array&lt;{
    card: string;
    amount: number;
  }&gt; = [];

  async charge(
    card: string,
    amount: number,
  ): Promise&lt;void&gt; {
    this.calls.push({
      card,
      amount,
    });
  }
}
</code></pre>
<p>Now you can compare the intention to charge without charging a customer twice.</p>
<h2 id="heading-how-to-test-the-architecture-you-actually-want">How to Test the Architecture You Actually Want</h2>
<p>Behavioral compatibility isn't enough if one objective of the migration is improving the architecture.</p>
<p>Imagine that you've decided on this constraint:</p>
<blockquote>
<p>Domain code must not depend on infrastructure code.</p>
</blockquote>
<p>If that rule only exists in an architecture diagram, migration pressure will eventually break it.</p>
<p>So test it.</p>
<p>For a simple project, you can inspect imports. For a larger one, use a dependency-analysis tool capable of enforcing architectural rules.</p>
<p>The exact tooling matters less than the principle:</p>
<p><strong>If an architectural constraint matters, make breaking it visible.</strong></p>
<p>You may want rules such as:</p>
<ul>
<li><p>domain must not depend on infrastructure</p>
</li>
<li><p>domain must not depend on the HTTP framework</p>
</li>
<li><p>application code must not depend directly on the database driver</p>
</li>
<li><p>modules must not import another module's internal implementation</p>
</li>
</ul>
<p>Why does this matter in an AI-assisted migration? Because AI is very good at finding a way to make code compile.</p>
<p>If reaching directly into another module solves the immediate problem, generated code may do exactly that unless the boundary is part of the constraints.</p>
<p>Architecture tests give both humans and AI tooling a harder boundary to violate accidentally.</p>
<h2 id="heading-how-to-decide-which-tasks-ai-should-handle">How to Decide Which Tasks AI Should Handle</h2>
<p>I don't treat all migration tasks equally. Some are good candidates for automation.</p>
<h3 id="heading-tasks-where-ai-is-usually-useful">Tasks Where AI Is Usually Useful</h3>
<ul>
<li><p>explaining unfamiliar code</p>
</li>
<li><p>identifying dependencies</p>
</li>
<li><p>extracting candidate business rules</p>
</li>
<li><p>generating characterization test cases</p>
</li>
<li><p>generating repetitive adapters</p>
</li>
<li><p>updating framework APIs</p>
</li>
<li><p>translating mechanical code</p>
</li>
<li><p>creating migration checklists</p>
</li>
<li><p>comparing implementations</p>
</li>
<li><p>classifying regression output</p>
</li>
<li><p>drafting technical documentation</p>
</li>
</ul>
<h3 id="heading-tasks-where-i-want-significant-engineering-review">Tasks Where I Want Significant Engineering Review</h3>
<ul>
<li><p>proposing module boundaries</p>
</li>
<li><p>extracting domain concepts</p>
</li>
<li><p>refactoring highly coupled classes</p>
</li>
<li><p>choosing migration sequences</p>
</li>
<li><p>changing data models</p>
</li>
<li><p>designing integration boundaries</p>
</li>
</ul>
<h3 id="heading-decisions-i-would-keep-under-human-ownership">Decisions I Would Keep Under Human Ownership</h3>
<ul>
<li><p>target architecture</p>
</li>
<li><p>acceptable behavioral differences</p>
</li>
<li><p>security boundaries</p>
</li>
<li><p>data migration strategy</p>
</li>
<li><p>rollout strategy</p>
</li>
<li><p>rollback strategy</p>
</li>
<li><p>removal of legacy behavior</p>
</li>
<li><p>production risk acceptance</p>
</li>
</ul>
<p>This isn't because AI can't produce an architecture proposal. It can.</p>
<p>The problem is accountability and context.</p>
<p>Architecture choices are consequences of constraints, history, organizational capabilities, business priorities, and operational risks that may not exist anywhere in the repository.</p>
<p>A model can help you explore those choices, but someone still has to own them.</p>
<h2 id="heading-how-to-measure-whether-the-migration-actually-improved-the-system">How to Measure Whether the Migration Actually Improved the System</h2>
<p>Migration velocity is an attractive metric because it's easy to show.</p>
<p>For example:</p>
<blockquote>
<p>37% of the codebase migrated.</p>
</blockquote>
<p>That doesn't tell you much about whether the system became better.</p>
<p>A modernization effort should look at several kinds of outcomes. Operational metrics might include:</p>
<ul>
<li><p>deployment frequency</p>
</li>
<li><p>change failure rate</p>
</li>
<li><p>mean time to recovery</p>
</li>
<li><p>production incidents</p>
</li>
<li><p>build time</p>
</li>
</ul>
<p>Engineering metrics might include:</p>
<ul>
<li><p>test coverage</p>
</li>
<li><p>high-complexity classes</p>
</li>
<li><p>duplicated business rules</p>
</li>
<li><p>cross-module dependencies</p>
</li>
<li><p>architectural violations</p>
</li>
<li><p>time required to change a capability</p>
</li>
</ul>
<p>Migration-specific metrics might include:</p>
<ul>
<li><p>regression rate</p>
</li>
<li><p>percentage of traffic handled by the new path</p>
</li>
<li><p>unresolved behavioral mismatches</p>
</li>
<li><p>rollback frequency</p>
</li>
<li><p>legacy components still in use</p>
</li>
</ul>
<p>The exact metrics depend on the system. What matters is avoiding this definition of success:</p>
<blockquote>
<p>Old repository is smaller = modernization succeeded.</p>
</blockquote>
<p>AI makes it possible to transform more code in less time. That makes measuring the quality of the transformation more important, not less.</p>
<h2 id="heading-the-risk-i-worry-about-most-with-ai-assisted-migration">The Risk I Worry About Most with AI-Assisted Migration</h2>
<p>Hallucinated code is a clear risk. But I worry more about <strong>plausible code</strong>.</p>
<p>Generated code can compile. It can look cleaner than the original implementation. It can even pass a shallow test suite. And it can still subtly change a business rule that nobody realized existed.</p>
<p>Consider something as small as:</p>
<pre><code class="language-typescript">if (customer.balance &gt; 0) {
  charge(customer);
}
</code></pre>
<p>It's tempting to clean up code when you don't understand why a condition exists.</p>
<p>But maybe zero has a special business meaning.</p>
<p>Maybe negative balances are legitimate.</p>
<p>Maybe the condition was introduced after a production incident six years ago and never documented.</p>
<p>AI can't recover context that doesn't exist in the information available to it. This is why I put so much emphasis on characterization tests and behavioral comparison.</p>
<p><strong>The faster the transformation becomes, the stronger the validation process needs to become.</strong></p>
<p>Otherwise, you're only increasing the speed at which you can introduce unknown changes.</p>
<h2 id="heading-a-practical-migration-workflow">A Practical Migration Workflow</h2>
<p>If I had to reduce the process to one repeatable sequence, I would use this.</p>
<h3 id="heading-1-understand">1. Understand</h3>
<p>Map:</p>
<ul>
<li><p>behavior</p>
</li>
<li><p>dependencies</p>
</li>
<li><p>business rules</p>
</li>
<li><p>side effects</p>
</li>
<li><p>data</p>
</li>
<li><p>integrations</p>
</li>
</ul>
<p>Use AI to accelerate the investigation. Don't start by generating the new system.</p>
<h3 id="heading-2-protect">2. Protect</h3>
<p>Build:</p>
<ul>
<li><p>characterization tests</p>
</li>
<li><p>integration tests</p>
</li>
<li><p>API fixtures</p>
</li>
<li><p>behavioral snapshots</p>
</li>
</ul>
<p>Make current behavior observable.</p>
<h3 id="heading-3-design">3. Design</h3>
<p>Choose:</p>
<ul>
<li><p>boundaries</p>
</li>
<li><p>interfaces</p>
</li>
<li><p>responsibilities</p>
</li>
<li><p>migration seams</p>
</li>
</ul>
<p>Do this before large-scale transformation.</p>
<h3 id="heading-4-refactor">4. Refactor</h3>
<p>Create enough separation that part of the system can move without dragging everything else with it.</p>
<h3 id="heading-5-transform">5. Transform</h3>
<p>Use AI heavily for repetitive implementation work.</p>
<p>Give it explicit architectural constraints.</p>
<h3 id="heading-6-compare">6. Compare</h3>
<p>Run old and new behavior against the same inputs and investigate differences.</p>
<h3 id="heading-7-release-gradually">7. Release Gradually</h3>
<p>Use the mechanisms appropriate for your environment:</p>
<ul>
<li><p>feature flags</p>
</li>
<li><p>canary deployments</p>
</li>
<li><p>shadow traffic</p>
</li>
<li><p>observability</p>
</li>
<li><p>rollback</p>
</li>
</ul>
<h3 id="heading-8-remove-the-old-path">8. Remove the Old Path</h3>
<p>Don't leave both systems running indefinitely. A migration that never removes the legacy path eventually creates another legacy architecture.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>AI changes the economics of legacy modernization.</p>
<p>A lot of work that used to consume engineering hours can now happen much faster: reading unfamiliar code, generating tests, updating APIs, translating repetitive implementations, and investigating differences between systems.</p>
<p>That's useful. But it's not the part of modernization that requires the most judgment.</p>
<p>The difficult questions remain:</p>
<ul>
<li><p>What behavior still matters?</p>
</li>
<li><p>What should disappear?</p>
</li>
<li><p>Which dependencies should survive?</p>
</li>
<li><p>Where should the boundaries be?</p>
</li>
<li><p>How much behavioral change is acceptable?</p>
</li>
<li><p>When is the new implementation safe enough to receive production traffic?</p>
</li>
</ul>
<p>If you use AI only to translate code, you can migrate technical debt faster.</p>
<p>If you combine it with characterization testing, incremental refactoring, explicit architectural boundaries, differential testing, and controlled rollout, you have a better chance of improving the system while you move it.</p>
<p>The objective isn't to move the same system onto a newer stack. It's to understand it, protect its important behavior, refactor it, migrate it incrementally, validate the result, and end up with a simpler system than the one you started with.</p>
<p>AI can shorten that path. But it still can't decide what the destination should be.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Stop Trusting AI Code Blindly: A React Code Refactoring Case Study ]]>
                </title>
                <description>
                    <![CDATA[ If you're a developer (or even a little bit familiar with all the AI developments of the past few years), the term Vibe Coding shouldn't be new to you. It is a software development practice where you  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/stop-trusting-ai-code-blindly-a-react-code-refactoring-case-study/</link>
                <guid isPermaLink="false">6a2054b908e3e46121ab26ae</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ refactoring ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tapas Adhikary ]]>
                </dc:creator>
                <pubDate>Wed, 03 Jun 2026 16:22:17 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/87edcb4f-6985-4392-8af5-b0f7daff9f5b.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you're a developer (or even a little bit familiar with all the AI developments of the past few years), the term <code>Vibe Coding</code> shouldn't be new to you. It is a software development practice where you describe what you want to AI (an LLM) in plain English, and in response, it gives you the source code for it.</p>
<p>You don't write anything manually line-by-line. You just completely focus on the vibe, like features, look-and-feel, and so on – and the AI generates the actual code for you. It's amazing and powerful.</p>
<p>Like millions of other software developers, I use and advocate the use of AI to a great extent. We should be using AI as a tool to expedite deliverables, to get repetitive work done, to make boilerplate, and anything that AI can help us with to stay productive.</p>
<p>But we shouldn't be doing any of this blindly, especially when it comes to delivering AI-generated work to customers.</p>
<p>All the modern AI tools like Claude, Gemini, or ChatGPT provide a warning upfront that AI can make mistakes. And we as users must double-check the responses before using them. Here's a similar notice from Claude:</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/060451dc-a9d4-45d2-95b7-0b22a11cc29b.png" alt="Claude AI notice" style="display: block;" width="701" height="47" loading="lazy">

<p>The main message is this: don't trust AI-generated code blindly. You must do your due diligence before you think of pushing it to production.</p>
<p>To illustrate this, in this article you'll learn from a recent case study I did on generating some React-based source code for an Analytics Dashboard app with AI.</p>
<p>The AI gave me some error-free source code that I could run to see the app. But when I started digging deeper into it, I found potential bugs and tech debt that I needed to address. The generated source code was far from being ready for production and needed a great deal of refactoring.</p>
<p>This guide is also available as a video tutorial as part of the <a href="https://www.youtube.com/playlist?list=PLIJrr73KDmRwySan3tObLmLZp0NYWSmCT">Full-Stack: Vibe Coding to Production Ready</a> series. You can check it out if you’d like:</p>
<div class="embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/NMkUVKue2jk" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>

<p>Let's start.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-the-prompt">The Prompt</a></p>
</li>
<li><p><a href="#heading-the-generated-react-code">The Generated React Code</a></p>
</li>
<li><p><a href="#heading-the-dashboard-app">The Dashboard App</a></p>
</li>
<li><p><a href="#heading-the-code-walkthrough-and-identifying-problems">The Code Walkthrough and Identifying Problems</a></p>
<ul>
<li><p><a href="#heading-problem-1-the-god-component-syndrome">Problem 1: The God Component Syndrome</a></p>
</li>
<li><p><a href="#heading-problem-2-the-state-soup-problem">Problem 2: The State Soup Problem</a></p>
</li>
<li><p><a href="#heading-problem-3-the-data-fetching-anti-pattern">Problem 3: The Data Fetching Anti-Pattern</a></p>
</li>
<li><p><a href="#heading-problem-4-the-missing-types-problem">Problem 4: The Missing Types Problem</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-refactoring-the-ai-generated-code">Refactoring the AI-Generated Code</a></p>
<ul>
<li><p><a href="#heading-refactoring-strategy">Refactoring Strategy</a></p>
</li>
<li><p><a href="#heading-define-types">Define Types</a></p>
</li>
<li><p><a href="#heading-break-the-monoliths">Break the Monoliths</a></p>
</li>
<li><p><a href="#heading-custom-hook-to-handle-data">Custom Hook to Handle Data</a></p>
</li>
<li><p><a href="#heading-everything-together">Everything Together</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-a-task-for-you">A Task for You</a></p>
</li>
<li><p><a href="#heading-key-takeaways">Key Takeaways</a></p>
</li>
<li><p><a href="#heading-if-youve-read-this-far">If You've Read This Far...</a></p>
</li>
</ol>
<h2 id="heading-the-prompt">The Prompt</h2>
<p>First, we need a prompt to inform the AI in plain English that it should generate the source code for the Analytics Dashboard.</p>
<p>Here is the prompt – read it carefully:</p>
<pre><code class="language-markdown">Act as an expert React developer. 

I need a complex 'Creator Analytics Dashboard' for a video platform created using React.

It should include: 

- 1. A header with a user profile. 
- 2. Three summary cards showing total views, videos, and comments. 
- 3. A data table showing recent videos with their individual stats. 
- 4. A sidebar with navigation options. 

Use Tailwind CSS for styling. 

Fetch fake data for the dashboard using fetch with a 1-second timeout to simulate a network request. 

Make it in a way so that I can copy and paste it easily.
</code></pre>
<p>It's a straightforward request to create a Creator Analytics Dashboard with a bunch of feature requests using React and TailwindCSS.</p>
<p>Now, if we take up this prompt and use it on any of the AI tools like ChatGPT, Claude, or Gemini, it will generate the source code for us automatically.</p>
<h2 id="heading-the-generated-react-code">The Generated React Code</h2>
<p>Here's the AI-generated source code from the prompt. As you can see, it's a complete React component with state management, side effect management, and JSX.</p>
<pre><code class="language-typescript">
import { useEffect, useState } from "react";

export default function CreatorDashboard() {
  const [stats, setStats] = useState({ views: 0, subs: 0, revenue: 0 });
  const [videos, setVideos] = useState([]);
  const [comments, setComments] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() =&gt; {
    let isMounted = true;

    const fetchData = async () =&gt; {
      try {
        await new Promise((resolve) =&gt; setTimeout(resolve, 1000));

        if (isMounted) {
          setStats({ views: '1.2M', subs: '45K', revenue: '$3,400' });
          setVideos([
            { id: 1, title: 'Vibe Coding Explained', views: '100K' },
            { id: 2, title: 'React 19 Features', views: '85K' }
          ]);
          setComments([{ id: 1, text: 'Great video!' }]);
          setIsLoading(false);
        }
      } catch (err) {
        setError('Failed to fetch data');
        setIsLoading(false);
      }
    };

    fetchData();
    return () =&gt; { isMounted = false; };
  }, []);

  if (isLoading) return &lt;div className="p-10 text-center text-xl"&gt;Loading Dashboard...&lt;/div&gt;;
  if (error) return &lt;div className="text-red-500 p-10"&gt;{error}&lt;/div&gt;;

  return (
    &lt;div className="flex bg-gray-100 min-h-screen"&gt;
      
      &lt;div className="w-64 bg-white shadow-md p-4"&gt;
        &lt;h2 className="text-2xl font-bold mb-6 text-blue-600"&gt;tapaScript&lt;/h2&gt;
        &lt;ul&gt;
           &lt;li className="mb-2 p-2 bg-blue-50 text-blue-700 rounded cursor-pointer"&gt;Dashboard&lt;/li&gt;
           &lt;li className="mb-2 p-2 hover:bg-gray-50 rounded cursor-pointer"&gt;Analytics&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/div&gt;

      &lt;div className="flex-1 p-8"&gt;
        
        &lt;header className="flex justify-between items-center mb-8"&gt;
          &lt;h1 className="text-3xl font-bold text-gray-800"&gt;Creator Analytics&lt;/h1&gt;
          &lt;div className="flex items-center gap-2"&gt;
            &lt;div className="w-10 h-10 bg-blue-500 rounded-full"&gt;&lt;/div&gt;
            &lt;span className="font-semibold"&gt;Tapas Adhikary&lt;/span&gt;
          &lt;/div&gt;
        &lt;/header&gt;

        
        &lt;div className="grid grid-cols-3 gap-6 mb-8"&gt;
          &lt;div className="bg-white p-6 rounded-lg shadow-sm border border-gray-100"&gt;
            &lt;h3 className="text-gray-500 text-sm font-medium"&gt;Total Views&lt;/h3&gt;
            &lt;p className="text-3xl font-bold mt-2"&gt;{stats.views}&lt;/p&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div className="grid grid-cols-3 gap-8"&gt;
           &lt;div className="col-span-2 bg-white rounded-lg shadow-sm p-6"&gt;
              &lt;h2 className="text-xl font-bold mb-4"&gt;Recent Videos&lt;/h2&gt;
              {videos.map(v =&gt; &lt;div key={v.id} className="border-b p-3"&gt;{v.title}&lt;/div&gt;)}
           &lt;/div&gt;
           &lt;div className="bg-white rounded-lg shadow-sm p-6"&gt;
              &lt;h2 className="text-xl font-bold mb-4"&gt;Recent Comments&lt;/h2&gt;
              {comments.map(c =&gt; &lt;div key={c.id} className="border-b p-3 text-sm text-gray-600"&gt;{c.text}&lt;/div&gt;)}
           &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>Note that if you try the same prompt again, it will generate slightly different source code as the LLM's responses are probabilistic and non-deterministic. It can produce different responses for the same prompt across multiple calls.</p>
<p>Alright, let's try out the generated code.</p>
<h2 id="heading-the-dashboard-app">The Dashboard App</h2>
<p>Now, copy that AI-generated code and paste it into any React project. When you run it, you should see a beautiful Creator Analytics Dashboard matching the functionalities mentioned in the prompt.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/c28d7c3c-bda8-46b2-91d7-4fb905207823.png" alt="Dashboard UI" style="display: block;" width="2441" height="1121" loading="lazy">

<p>This is amazing and powerful. As a developer, we must leverage it as much as possible. But as a developer, you also need to act like human guardrails to make sure that the generated code is modular, scalable, and bug-free.</p>
<p>Let's now do the walkthrough of the AI-generated code.</p>
<h2 id="heading-the-code-walkthrough-and-identifying-problems">The Code Walkthrough and Identifying Problems</h2>
<p>Before you read further, go back and read the generated source code once more. This time, slowly, carefully – like a code reviewer.</p>
<p>What have you found? Let's see if your findings match the list from my case study.</p>
<h3 id="heading-problem-1-the-god-component-syndrome">Problem 1: The God Component Syndrome</h3>
<p>In software engineering, we have the <code>Single Responsibility Principle(SRP)</code>. It means a function or component should do exactly one thing.</p>
<p>But here, our <code>CreatorDashboard</code> is acting as a "God Component". It manages state, it fetches data from the network, it renders the sidebar, it renders the header, the card, the tables...everything.</p>
<p>If the marketing team asks you to reuse that Stats Card on the marketing landing page, you simply can't. You need to rewrite it, as it's locked inside the giant file.</p>
<h3 id="heading-problem-2-the-state-soup-problem">Problem 2: The State Soup Problem</h3>
<p>Look at the top of the component. Five different <code>useState</code> declarations. When a component renders, tracking which piece of text triggered it becomes a nightmare. This should either be grouped or, even better, managed by a dedicated data fetching library like TanStack Query.</p>
<p>Remember, the fewer states you manage in your component, the better your life will be as a React developer.</p>
<h3 id="heading-problem-3-the-data-fetching-anti-pattern">Problem 3: The Data Fetching Anti-Pattern</h3>
<p>AI loves to use <code>useEffect</code> for data fetching. It's one of the biggest anti-patterns in modern React. This is because the hook useEffect was never meant for data fetching. It doesn't handle caching, it doesn't handle retries if the network drops, and if the user navigates away and comes back, it forces a hard reload on the data every single time.</p>
<p>Modern React provides a better mechanism for data fetching. I've written a <a href="https://www.freecodecamp.org/news/the-modern-react-data-fetching-handbook-suspense-use-and-errorboundary-explained/">Handbook on how to use Suspense and Error Boundary</a> to handle data fetching in React. You can give it a read.</p>
<h3 id="heading-problem-4-the-missing-types-problem">Problem 4: The Missing Types Problem</h3>
<p>We haven't mentioned TypeScript explicitly in the prompt. So, AI gave us JavaScript by default. Now, the problem is, can we guarantee what the <code>videos</code> array holds? What does a video object look like? We don't know, and our editor also can't help us.</p>
<h2 id="heading-refactoring-the-ai-generated-code">Refactoring the AI-Generated Code</h2>
<p>Now that we've identified the problems, the next logical step is to refactor the code to make it better.</p>
<h3 id="heading-refactoring-strategy">Refactoring Strategy</h3>
<p>The image below shows the refactoring strategy we'll follow. We'll break the giant AI-generated component into logical, smaller components like Header, Sidebar, RecentComments, and so on.</p>
<p>We also need to handle the data outside of the component and make the data fetching mechanism reusable for other components in the application to leverage it. To do this, we'll apply the <code>Custom Hook Pattern</code>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/15e028fc-efd0-428b-bfde-7d851634bfbf.png" alt="Refactored code strategy" style="display: block;" width="1534" height="942" loading="lazy">

<h3 id="heading-define-types">Define Types</h3>
<p>First, let's define all the types needed for the data objects. We need type definitions for video status, comments, and overall creator status.</p>
<pre><code class="language-typescript">
// We use 'type' or 'interface' in TypeScript to define the shape of an object.

export interface CreatorStat {
  label: string;
  value: string | number;
}

export interface VideoStats {
  id: string; // ID should always be a string (UUID) or number, we'll enforce string here
  title: string;
  views: number;
  publishedAt: string;
}

export interface Comment {
  id: string;
  author: string;
  text: string;
  createdAt: string;
}
</code></pre>
<h3 id="heading-break-the-monoliths">Break the Monoliths</h3>
<p>Next, we'll solve the problem of SRP violation and the problem of <code>CreatorDashboard</code> being a God Component. Refactor the giant component by breaking it into multiple smaller components:</p>
<ul>
<li><strong>Header</strong>: A component represents the header of the analytics dashboard.</li>
</ul>
<pre><code class="language-typescript">function Header() {
    return (
        &lt;header className="flex justify-between items-center mb-8"&gt;
            &lt;h1 className="text-3xl font-bold text-gray-800"&gt;
                Creator Analytics
            &lt;/h1&gt;
            &lt;div className="flex items-center gap-2"&gt;
                &lt;div className="w-10 h-10 bg-blue-500 rounded-full"&gt;&lt;/div&gt;
                &lt;span className="font-semibold"&gt;Tapas Adhikary&lt;/span&gt;
            &lt;/div&gt;
        &lt;/header&gt;
    );
}

export default Header;
</code></pre>
<ul>
<li><strong>Sidebar</strong>: The sidebar component holds the navigation links.</li>
</ul>
<pre><code class="language-typescript">export default function Sidebar() {
    return (
        &lt;div className="w-64 bg-white shadow-md p-4"&gt;
            &lt;h2 className="text-2xl font-bold mb-6 text-blue-600"&gt;
                tapaScript
            &lt;/h2&gt;
            &lt;ul&gt;
                &lt;li className="mb-2 p-2 bg-blue-50 text-blue-700 rounded cursor-pointer"&gt;
                    Dashboard
                &lt;/li&gt;
                &lt;li className="mb-2 p-2 hover:bg-gray-50 rounded cursor-pointer"&gt;
                    Analytics
                &lt;/li&gt;
            &lt;/ul&gt;
        &lt;/div&gt;
    );
}
</code></pre>
<ul>
<li><strong>StatCard</strong>: This component accepts a status label and value and renders them. Note how we've applied the types here on the label and value props.</li>
</ul>
<pre><code class="language-typescript">// 1. We define the Props interface.
// "Props" are the arguments passed into a React component.
// We are enforcing that whoever uses this component MUST pass a label and a value.
interface StatCardProps {
    label: string;
    value: string | number;
}

// 2. We extract the props cleanly using destructuring: { label, value }
function StatCard({ label, value }: StatCardProps) {
    return (
        &lt;div className="bg-white p-6 rounded-lg shadow-sm border border-gray-100 hover:shadow-md transition-shadow"&gt;
            &lt;h3 className="text-gray-500 text-sm font-medium uppercase tracking-wider"&gt;
                {label}
            &lt;/h3&gt;
            &lt;p className="text-3xl font-extrabold mt-2 text-gray-900"&gt;
                {value}
            &lt;/p&gt;
        &lt;/div&gt;
    );
}

export default StatCard;
</code></pre>
<ul>
<li><strong>VideoTable</strong>: This component lists out all the video information. So, it accepts an array of videos. Notice that we've solved the type problem here. Now we know that each video in the videos array is of the <code>VideoStats</code> type that we defined earlier.</li>
</ul>
<pre><code class="language-typescript">
import type { VideoStats } from '../types';

interface VideoTableProps {
  // We expect an array of VideoStats objects.
  videos: VideoStats[];
}

function VideoTable({ videos }: VideoTableProps) {
  if (videos.length === 0) {
    return &lt;div className="p-6 text-center text-gray-500"&gt;No videos uploaded yet.&lt;/div&gt;;
  }

  return (
    &lt;div className="bg-white rounded-lg shadow-sm border border-gray-100 overflow-hidden"&gt;
      &lt;div className="p-4 border-b border-gray-100 bg-gray-50"&gt;
        &lt;h2 className="text-lg font-bold text-gray-800"&gt;Recent Videos&lt;/h2&gt;
      &lt;/div&gt;
      &lt;ul className="divide-y divide-gray-100"&gt;
        {videos.map((video) =&gt; (
          &lt;li key={video.id} className="p-4 hover:bg-gray-50 flex justify-between items-center"&gt;
            &lt;span className="font-medium text-gray-900"&gt;{video.title}&lt;/span&gt;
            &lt;span className="text-sm bg-blue-100 text-blue-800 py-1 px-3 rounded-full font-semibold"&gt;
              {video.views.toLocaleString()} views
            &lt;/span&gt;
          &lt;/li&gt;
        ))}
      &lt;/ul&gt;
    &lt;/div&gt;
  );
}

export default VideoTable;
</code></pre>
<ul>
<li><strong>RecentComments</strong>: A component to show the list of comments.</li>
</ul>
<pre><code class="language-typescript">import type { Comment } from "../types";

interface RecentCommentProps {
    // We expect an array of Comment objects.
    videos: Comment[];
}

function RecentCommentList({ comments }: RecentCommentProps) {
    if (comments.length === 0) {
        return (
            &lt;div className="p-6 text-center text-gray-500"&gt;
                You don't have any comments posted.
            &lt;/div&gt;
        );
    }

    return (
        &lt;div className="bg-white rounded-lg shadow-sm p-6"&gt;
            &lt;h2 className="text-xl font-bold mb-4"&gt;Recent Comments&lt;/h2&gt;
            {comments.map((c) =&gt; (
                &lt;div key={c.id} className="border-b p-3 text-sm text-gray-600"&gt;
                    {c.text}
                &lt;/div&gt;
            ))}
        &lt;/div&gt;
    );
}

export default RecentCommentList;
</code></pre>
<h3 id="heading-custom-hook-to-handle-data">Custom Hook to Handle Data</h3>
<p>Now that we have the components defined, and all of them are presentational components, they need data to render information on the dashboard. Also, we don't want to handle all the states inside our component. A custom hook would be a great choice here.</p>
<p>The hook handles the fetch call to get analytics data and tracks them using the state. We return the needed state values from the hook so that anyone using the hook anywhere would get this information. It's completely reusable.</p>
<pre><code class="language-typescript">import { useEffect, useState } from "react";
import type { Comment, CreatorStat, VideoStats } from "./types";

export function useDashboardData() {
    const [stats, setStats] = useState&lt;CreatorStat[]&gt;([]);
    const [videos, setVideos] = useState&lt;VideoStats[]&gt;([]);
    const [comments, setComments] = useState&lt;Comment[]&gt;([]);
    const [isLoading, setIsLoading] = useState(true);
     const [error, setError] = useState&lt;string | null&gt;(null);

    useEffect(() =&gt; {
        let isMounted = true;

        const fetchData = async () =&gt; {
            try {
                // Simulating an API call
                await new Promise((resolve) =&gt; setTimeout(resolve, 1000));

                if (isMounted) {
                    setStats([
                        { label: "Views", value: "1.2M" },
                        { label: "subs", value: "45K" },
                        { label: "revenue", value: "$3,400" },
                    ]);
                    setVideos([
                        {
                            id: 1,
                            title: "Vibe Coding Explained",
                            views: "100K",
                        },
                        { id: 2, title: "React 19 Features", views: "85K" },
                    ]);
                    setComments([
                        { id: 1, text: "Great video!" },
                        { id: 2, text: "Fantastic video!" },
                    ]);
                    setIsLoading(false);
                }
            } catch (err) {
                setError(`Failed to fetch data: ${err?.message}`);
                setIsLoading(false);
            }
        };

        fetchData();
        return () =&gt; {
            isMounted = false;
        };
    }, []);

    return {
        stats,
        videos,
        comments,
        isLoading,
        error
    }

} 
</code></pre>
<h3 id="heading-everything-together">Everything Together</h3>
<p>Finally, it's time to change the giant <code>CreatorDashboard</code> component. We'll first import all the smaller components created, and then call the hook to get the stats, videos, comments, and loading and error states. After that, it's just about using them.</p>
<pre><code class="language-typescript">import Header from "@/components/Header";
import Sidebar from "@/components/Sidebar";
import RecentCommentList from "./components/RecentComments";
import StatCard from "./components/StatCard";
import VideoTable from "./components/VideoTable";

import { useDashboardData } from "./hooks/useDashboardData";

export default function CreatorDashboard() {
    const { stats, videos, comments, isLoading, error } = useDashboardData();

    if (isLoading)
        return (
            &lt;div className="p-10 text-center text-xl"&gt;Loading Dashboard...&lt;/div&gt;
        );
    if (error) return &lt;div className="text-red-500 p-10"&gt;{error}&lt;/div&gt;;

    return (
        &lt;div className="flex bg-gray-100 min-h-screen"&gt;
            {/* Sidebar Navigation */}
            &lt;Sidebar /&gt;

            &lt;div className="flex-1 p-8"&gt;
                {/* Header */}
                &lt;Header /&gt;

                {/* Stats Cards */}
                &lt;div className="grid grid-cols-3 gap-6 mb-8"&gt;
                    {stats.map((stat) =&gt; (
                        &lt;StatCard
                            key={stat.label}
                            label={stat.label}
                            value={stat.value}
                        /&gt;
                    ))}
                &lt;/div&gt;

                {/* Data Table &amp; Comments - All mashed together */}
                &lt;div className="grid grid-cols-3 gap-8"&gt;
                    &lt;div className="lg:col-span-2"&gt;
                        &lt;VideoTable videos={videos} /&gt;
                    &lt;/div&gt;
                    &lt;RecentCommentList comments={comments} /&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    );
}
</code></pre>
<p>That's all. We have now successfully refactored the big AI-generated component into smaller, reusable components and separated the data layer and state handling outside of it.</p>
<h2 id="heading-a-task-for-you">A Task for You</h2>
<p>This is optional, yet I'd encourage you to try it. The task is to take the refactoring to the next level.</p>
<p>Can you get rid of the <code>useDashboardData</code> hook, and handle the data fetching using the <a href="https://www.freecodecamp.org/news/the-modern-react-data-fetching-handbook-suspense-use-and-errorboundary-explained/">Suspense and Error Boundary patterns</a>? I would love to discuss the solution with you. Please reach out on my socials (given below) or my <a href="https://discord.gg/sSQ7HEYrrZ">Discord Server</a>.</p>
<p>Also, stay tuned for my upcoming article, where I'll refactor the same app with TanStack Query and teach you about fetch, mutation, and caching.</p>
<h2 id="heading-key-takeaways">Key Takeaways</h2>
<p>This is the reality of AI-generated code. It looks like a finished product on the surface. But underneath, it's a fragile house of cards. If you try to scale this, say by adding authentication, sorting to the tables, or real-time comment updates, the file will grow to 1K+ lines of unmaintainable code.</p>
<p>Our job isn't to reject AI's output. Instead, it's to refactor its output to make it production-ready. You can do that only when you have strong fundamentals, and you understand the <a href="https://www.freecodecamp.org/news/the-new-definition-of-software-engineering-in-the-age-of-ai/">new definition of software engineering in the age of AI</a>.</p>
<h2 id="heading-if-youve-read-this-far"><strong>If You've Read This Far...</strong></h2>
<p>Thank You!</p>
<p>I'm thrilled to announce that I've started a <a href="https://www.youtube.com/playlist?list=PLIJrr73KDmRwySan3tObLmLZp0NYWSmCT">Full Stack FREE Course</a> to take developers from vibe coding to a production-ready mental model. I'd be delighted if you check it out and take part.</p>
<ul>
<li><p>Subscribe to my <a href="https://www.youtube.com/tapasadhikary?sub_confirmation=1">YouTube Channel</a></p>
</li>
<li><p>Follow on <a href="https://www.linkedin.com/in/tapasadhikary/">LinkedIn</a> and <a href="https://x.com/tapasadhikary">X</a></p>
</li>
<li><p>Catch up with my <a href="https://www.tapascript.io/books/react-clean-code-rule-book">React Clean Code Rules Book</a></p>
</li>
<li><p>All the source code used in this article is on my <a href="https://github.com/tapascript/full-stack-vibe-to-prod">GitHub Repository</a>.</p>
</li>
</ul>
<p>See you soon with my next article. Until then, please take care of yourself and keep learning.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Refactor Complex Codebases – A Practical Guide for Devs ]]>
                </title>
                <description>
                    <![CDATA[ Developers often see refactoring as a secondary concern that they can delay indefinitely because it doesn’t immediately contribute to revenue or feature development. And managers frequently view refactoring as "not a business need" until it boils ove... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-refactor-complex-codebases/</link>
                <guid isPermaLink="false">682df5a0f2057ab279952dbe</guid>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ webdev ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ General Programming ]]>
                    </category>
                
                    <category>
                        <![CDATA[ code review ]]>
                    </category>
                
                    <category>
                        <![CDATA[ refactoring ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Ankur Tyagi ]]>
                </dc:creator>
                <pubDate>Wed, 21 May 2025 15:47:44 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1747835131515/f6ea465a-9b14-4918-8943-87ec225b19b3.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Developers often see refactoring as a secondary concern that they can delay indefinitely because it doesn’t immediately contribute to revenue or feature development.</p>
<p>And managers frequently view refactoring as "not a business need" until it boils over and becomes the most significant business need possible.</p>
<blockquote>
<p><em>"Oh, our software somehow works. We can't implement any new changes. And oh, everyone is quitting because work is miserable."</em></p>
</blockquote>
<p>In this article, I’ll walk you through the steps I use to refactor a complex codebase. We’ll talk about setting goals, writing tests, breaking up monoliths into smaller modules, verifying changes, making sure existing features still work, and keeping tabs on performance. I’ll also show you how to speed up reviews using AI tools.</p>
<p>By following these steps, you can turn complex, fragile code into a clean, reliable codebase your team can own.</p>
<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXccvZ3sehF8oGifjnapnY9AUcPde9aKy9t_YEUeL8M2s3dcwxFq_bJLCSp_S02fIvfbwzpZfkz7e-2JQpXpzcdqELqs80EjkLLRpz0Uat6q9_RcRM5VQbjLoUxA2GlaqyeolsKGeA?key=nBTgfzmVkL2-N7DBMJ6e6gyk" alt="code-refactoring" width="800" height="450" loading="lazy"></p>
<h2 id="heading-the-issue-of-technical-debt">The Issue of Technical Debt</h2>
<p>As projects grow and evolve, <a target="_blank" href="https://en.wikipedia.org/wiki/Technical_debt">technical debt</a> increases. Code that was once functional and manageable turns into an unmaintainable mess, where even small changes become risky and time-consuming.</p>
<p>Despite the obvious need for cleanup, refactoring rarely gets prioritized because there's always something more urgent, new features, bug fixes, and client demands.</p>
<p>I’ve had conversations with engineers, many of whom are working on enterprise software and are fully aware of their codebase's code smells and inconsistencies. They dislike the situation but feel powerless to change it.</p>
<p>So how do we shift from a culture of writing for pure functionality to a culture that values maintainability, especially for complex codebases?</p>
<p>It’s usually a mistake to completely halt new feature development for a long refactoring period (except perhaps in emergencies). Business needs still exist, and putting everything on hold can create tension and lost opportunities. It’s better to find a balance so you’re still delivering value to users even as you clean under the hood.</p>
<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXeZx-XKCA2DC6kQQe2-4NU07wKEm0_VZ4kqEjbF6u2vy2paRigdNRUGjr-_AoE6ueNjCxNjnB-mI7uroXFhJ0nFfvWzwYq2VUMsdsPhXu4KvGYSZcUN0nFmKg8U8WzgGJQAgKtUaw?key=nBTgfzmVkL2-N7DBMJ6e6gyk" alt="Uncle-bob-take-on-refactoring" width="1600" height="518" loading="lazy"></p>
<p>While there is no one-size-fits-all solution, a structured approach can help teams introduce sustainable refactoring practices, even in environments where management is resistant. Let’s explore how this works.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents:</strong></h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-refactoring">What is Refactoring?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-preparing-for-refactoring">Preparing for Refactoring</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-secure-management-buy-in">Secure Management Buy-in</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-ensure-a-safety-net-with-automated-testing">Ensure a Safety Net with Automated Testing</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-identify-high-risk-areas">Identify High-Risk Areas</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-set-clear-refactoring-goals">Set Clear Refactoring Goals</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-techniques-for-refactoring-complex-codebases">Techniques for Refactoring Complex Codebases</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-identifying-and-isolating-problem-areas">1. Identifying and Isolating Problem Areas</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-incremental-vs-big-bang-refactoring">2. Incremental vs. Big Bang Refactoring</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-breaking-down-monolithic-code">3. Breaking Down Monolithic Code</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-4-ensuring-backward-compatibility">4. Ensuring Backward Compatibility</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-5-handling-dependencies-and-tight-coupling">5. Handling Dependencies and Tight Coupling</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-6-testing-strategiessafely-refactoring-with-confidence">6. Testing Strategies (Safely Refactoring with Confidence)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-7-refactoring-without-breaking-performance">7. Refactoring Without Breaking Performance</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-8-automate-code-reviews-with-ai-tools">8. Automate Code Reviews with AI Tools</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-summary">Summary</a></p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-what-is-refactoring"><strong>What is Refactoring?</strong></h2>
<p>Many people all too often use the word "refactor" when they mean a targeted rewrite.</p>
<p>As Martin Fowler famously said,</p>
<blockquote>
<p><em>“Refactoring is a controlled technique for improving the design of an existing code base. Its essence is applying a series of small behavior-preserving transformations... However, the cumulative effect... is quite significant.”</em>​</p>
</blockquote>
<p>In practice, this means continuously polishing code to reduce complexity and technical debt.</p>
<p>While traditional software development follows a linear approach of designing first and coding second, real-world projects often evolve in ways that lead to structural decay. Refactoring counteracts this by continuously refining the codebase, transforming disorganized or inefficient implementations into well-structured, maintainable solutions.</p>
<p>A targeted rewrite is a focused overhaul of a specific aspect of an application, often affecting multiple parts of the codebase. It carries more risk than refactoring but is still controlled and contained.</p>
<h2 id="heading-preparing-for-refactoring">Preparing for Refactoring</h2>
<p>Even the most skilled refactoring effort can stall without proper preparation. Before you start moving code around, laying a foundation that will keep your work organized and your team on the same page is crucial.</p>
<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXcr3hNpzC9XPUVnG6d7uHuC977aYrG2VVOH-8E4WhzM5Rfz3vzPDUPTwJChrK0l7WUK8BLTzYr5-295_27ARWQvcmjufXOk68Bg8szUjEq3IFVCDO0XfTSRFy1LaxqyjvjVDNddsw?key=nBTgfzmVkL2-N7DBMJ6e6gyk" alt="martin-fowler-on-refactoring" width="1600" height="364" loading="lazy"></p>
<p>Here are some steps you can take to ensure your refactoring efforts are successful.</p>
<h3 id="heading-secure-management-buy-in">Secure Management Buy-in</h3>
<p>As I’ve already discussed, getting time for refactoring can be difficult in feature-driven organizations. Often, management will accept refactoring investment if you can tie it to business outcomes, faster time to market, fewer outages (which translates to happier customers), and the ability to take on new initiatives.</p>
<p>Make those connections explicit. For example, you could say:</p>
<blockquote>
<p><em>“If we refactor our reporting engine now, it will make it feasible to add the analytics module next quarter, which unlocks a new revenue stream.”</em></p>
</blockquote>
<p>Or use data:</p>
<blockquote>
<p><em>“We spent 30% of our last sprint fixing bugs in module Y. After refactoring Y, we expect that to drop significantly, freeing time for new features.”</em></p>
</blockquote>
<p>Business-minded arguments help justify the balance.</p>
<h3 id="heading-ensure-a-safety-net-with-automated-testing">Ensure a Safety Net with Automated Testing</h3>
<p>As you refactor, tests are your safety net. Before modifying a component, write characterization tests around it if they don’t exist.</p>
<pre><code class="lang-python"><span class="hljs-comment"># example: characterization test for a legacy function</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">legacy_calculate_discount</span>(<span class="hljs-params">price, rate</span>):</span>
    <span class="hljs-comment"># ... complex logic you don't fully understand yet ...</span>
    <span class="hljs-keyword">return</span> price * (<span class="hljs-number">1</span> - rate/<span class="hljs-number">100</span>) <span class="hljs-keyword">if</span> rate &lt; <span class="hljs-number">100</span> <span class="hljs-keyword">else</span> <span class="hljs-number">0</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_legacy_calculate_discount</span>():</span>
    <span class="hljs-comment"># capture existing behavior</span>
    <span class="hljs-keyword">assert</span> legacy_calculate_discount(<span class="hljs-number">100</span>, <span class="hljs-number">10</span>) == <span class="hljs-number">90</span>
    <span class="hljs-keyword">assert</span> legacy_calculate_discount(<span class="hljs-number">50</span>, <span class="hljs-number">200</span>) == <span class="hljs-number">0</span>
</code></pre>
<p>These tests capture the current behavior, so you’ll know if you accidentally change it. Unit tests, integration tests, and e2e tests all validate that refactoring hasn’t broken anything.</p>
<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXfWfke-9FxoQIPFwRWVoIWrYN7L40mEmhpdAUkcBm34mwzXJ0R8jXKH8rZ0HjAghAtQ-v6dTUYYvK0T8_QBgyfeab-7R50pnB6BgdDm9L4PkFwvwGlUYTHNo21f37fxMZYt3xeY?key=nBTgfzmVkL2-N7DBMJ6e6gyk" alt="automated-testing-is-imp-for-refactoring" width="1600" height="432" loading="lazy"></p>
<p>It’s often worth investing time in setting up a continuous integration pipeline so that every change triggers automated tests. This gives rapid feedback and confidence that you’re not introducing regressions. Robust testing and CI/CD enable you to move faster and refactor with peace of mind.</p>
<pre><code class="lang-powershell"><span class="hljs-comment"># .github/workflows/ci.yml</span>
name: CI
on: [<span class="hljs-type">push</span>, <span class="hljs-type">pull_request</span>]
jobs:
  test:
    runs<span class="hljs-literal">-on</span>: ubuntu<span class="hljs-literal">-latest</span>
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup<span class="hljs-literal">-python</span>@v4
        with: python<span class="hljs-literal">-version</span>: <span class="hljs-string">'3.10'</span>
      - run: pip install <span class="hljs-literal">-r</span> requirements.txt
      - run: pytest -<span class="hljs-literal">-maxfail</span>=<span class="hljs-number">1</span> -<span class="hljs-literal">-disable</span><span class="hljs-literal">-warnings</span> <span class="hljs-literal">-q</span>
</code></pre>
<h3 id="heading-identify-high-risk-areas">Identify High-Risk Areas</h3>
<p>The first step is to figure out what to refactor. High-risk areas are parts of the code likely to cause bugs or slow development. Common signs include long methods, large classes, duplicate code, and complex conditional logic​.</p>
<p>Such code “smells” often hint at deeper design problems. Tools like static analysis can automatically flag these issues.</p>
<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXfS4aFy2hyRSq3UmgB2gQ8NN_-yUksNXcSavTtpnL8KIiWpGGidCSCstLKANZGOjJLqEF69wp-xjMGH6jrjurSaFtUIMS09vUaDgJ6vGtyabP-4QC5ISmT_cMvaaw6c2KlyVa1CKQ?key=nBTgfzmVkL2-N7DBMJ6e6gyk" alt="SonarQube-dashboard" width="1600" height="743" loading="lazy"></p>
<p>For example, SonarQube will mark code smells (like high complexity or long methods) that increase technical debt​. Using SonarQube or similar tools, you can generate reports on code complexity (for example, cyclomatic complexity metrics​) and find hotspots in the codebase that need more attention.</p>
<h3 id="heading-set-clear-refactoring-goals">Set Clear Refactoring Goals</h3>
<p>Before refactoring code, define the goal.</p>
<p>Goals must be specific and measurable. For example, you might aim to reduce a class’s size or a function’s <a target="_blank" href="https://www.ibm.com/docs/en/raa/6.1.0?topic=metrics-cyclomatic-complexity">cyclomatic complexity</a> by a certain amount or to increase unit test coverage from 60% to 90%.</p>
<p>Each goal is tied to a measurable outcome: shorter methods, fewer if statements or classes with a single responsibility, faster execution for processing orders, higher test coverage, and no unused code. These targets will guide our refactoring plan and let us verify when we’ve succeeded.</p>
<p><strong>Tip:</strong> Write down your refactoring goals and share them with your team. This sets expectations that you’re not adding new features in this effort, just making the code cleaner and more robust. It also helps justify the time spent by showing the benefits (like more straightforward future additions and fewer bugs).</p>
<h2 id="heading-techniques-for-refactoring-complex-codebases">Techniques for Refactoring Complex Codebases</h2>
<h3 id="heading-1-identifying-and-isolating-problem-areas">1. Identifying and Isolating Problem Areas</h3>
<p>It can be overwhelming to decide where to start refactoring a large codebase. Not every part of the code needs refactoring – some areas are delicate or rarely touched.</p>
<p>The most impactful refactoring efforts typically target the “problem areas”: parts of the codebase that are overly complex, error-prone, or act as bottlenecks for development and performance. Identifying these areas is a crucial first step.</p>
<h3 id="heading-techniques-for-finding-hotspots">Techniques for Finding Hotspots</h3>
<h4 id="heading-team-knowledge-amp-developer-frustration">Team knowledge &amp; developer frustration</h4>
<p>Don’t underestimate the value of anecdotal information from the team. Which parts of the code do developers dread working in? Often, the team’s instincts point to areas that are hard to understand or modify (for example, “the accounting module is a black box, we hate touching it”). These could be areas to improve.</p>
<p>In my experience, simply asking, “If you had a magic wand, which part of the code would you rewrite?” yields very insightful answers.</p>
<h4 id="heading-code-complexity-metrics">Code complexity metrics</h4>
<p>Use static analysis tools to measure cyclomatic complexity, code duplication, large functions/classes, and so on. Files or modules with extremely high complexity numbers or thousands of lines are good candidates for scrutiny. But static complexity alone doesn’t tell the whole story – a file might be ugly but rarely touched.</p>
<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXc07SWwlu4GxU6AwoXQEHyyEcQY-6YMOEPr7b7Quhk5UvLD7qx9XyZla2SzP32eGFoYY_Xy-SYZQ9mOMX7Mxeq1YCnFXQxudsMNbvak9CLZfSOeRIvdll_pLW56sAmvRcPZMk36Rg?key=nBTgfzmVkL2-N7DBMJ6e6gyk" alt="SonarQube" width="1600" height="1030" loading="lazy"></p>
<h4 id="heading-change-frequency-churn">Change frequency (Churn)</h4>
<p>Look at version control history to see which files are often changed, especially those associated with bug fixes or incidents.</p>
<h4 id="heading-hotspot-analysis">Hotspot analysis</h4>
<p>A robust approach combines complexity and change frequency to find “hotspots.” For example, a tool or technique plotting modules by their complexity and how often they change can highlight the problematic areas. CodeScene (a code analysis tool) popularized this: <em>hotspots</em> are parts of the code that are highly complex and frequently modified, indicating areas where “paying down debt has a real impact”​.</p>
<p>If a module is a mess and developers are in it every week, improving that module will likely yield outsized benefits (fewer bugs, faster adds).</p>
<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXdJkGfbDK6UFDN9hqzeyCMBWmajADhAMJwzSouyMNz_63o9SRNfOly9AP_XiY2jqfi02fHSIFkMBCfstkjJfkxVB-NaHCSit0xssTYfztZ2BRQZmqYr_lTc3R750-1-lrJi7eeViQ?key=nBTgfzmVkL2-N7DBMJ6e6gyk" alt="code-health-dashboard" width="1542" height="1600" loading="lazy"></p>
<h4 id="heading-performance-bottlenecks-and-crashes">Performance bottlenecks and crashes</h4>
<p>Some parts of the codebase become targets for refactoring because they cause frequent performance problems or outages. For instance, if a specific service or job crashes often or can’t keep up with the load, you might need to refactor it for stability.</p>
<h3 id="heading-how-to-isolate-problem-areas">How to Isolate Problem Areas</h3>
<p>Once you’ve identified a hotspot or problem area, the next challenge is isolating it so you can refactor safely. In a complex system, nothing lives in complete isolation. That problematic module likely interacts with many others.</p>
<p>Here are strategies to isolate and tackle it:</p>
<h4 id="heading-break-dependencies-create-seams">Break dependencies (Create seams)</h4>
<p>Michael Feathers (in <em>Working Effectively with Legacy Code</em>) introduced the concept of “seams” – places where you can cut into a codebase to isolate a part for testing or refactoring. This might mean introducing an interface or abstraction between components so you can work on one side independently.  </p>
<p>For example, suppose PaymentService is tightly coupled to StripeGateway, with direct calls scattered throughout the code.</p>
<pre><code class="lang-python"><span class="hljs-comment"># payment_service.py</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">charge_customer</span>(<span class="hljs-params">order_id, amount</span>):</span>
    <span class="hljs-comment"># Hardcoded dependency to Stripe</span>
    stripe = StripeGateway()
    stripe.charge(order_id, amount)
</code></pre>
<p>To isolate and refactor the payment logic safely, you can introduce a <code>PaymentProcessor</code> interface and have <code>PaymentService</code> depend on that interface instead. Then, create an adapter like StripeAdapter that implements PaymentProcessor and delegates to the existing Stripe logic.</p>
<p>This way, you can safely refactor or even replace the Stripe integration behind the StripeAdapter without impacting <code>PaymentService</code> or any other module that uses it. As long as the <code>PaymentProcessor</code> interface is honored, the rest of the system remains unaffected.</p>
<pre><code class="lang-python"><span class="hljs-comment"># interfaces.py</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PaymentProcessor</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">charge</span>(<span class="hljs-params">self, order_id, amount</span>):</span>
        <span class="hljs-keyword">raise</span> NotImplementedError


<span class="hljs-comment"># stripe_adapter.py</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">StripeAdapter</span>(<span class="hljs-params">PaymentProcessor</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">charge</span>(<span class="hljs-params">self, order_id, amount</span>):</span>
        <span class="hljs-comment"># Internally still uses Stripe</span>
        stripe = StripeGateway()
        stripe.charge(order_id, amount)


<span class="hljs-comment"># payment_service.py (Refactored)</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PaymentService</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, processor: PaymentProcessor</span>):</span>
        self.processor = processor

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">charge_customer</span>(<span class="hljs-params">self, order_id, amount</span>):</span>
        self.processor.charge(order_id, amount)
</code></pre>
<h4 id="heading-branch-by-abstraction">“Branch-by-abstraction”</h4>
<p>This technique is related to the above and is often used in continuous delivery. The idea is to add a layer of abstraction (like an interface or proxy) in front of the old code, have both old and new code implementations behind it, and then gradually shift usage from the old to the new implementation. For a while, you might have a temporary state where both versions exist (perhaps toggled by a config or feature flag).</p>
<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXcaFoXSHVYTBz_1DOsucPkvwGQwfo9qrvhPYvvjYOXQsLIh2MCTfseB1g9SOfijpdKMwcwmK4lfPWcyhn4vf5gaFwdliKUZUGDOcQVJ0qupRLjvnhFrSm5LZfe8OoqZtZkHkj9IXw?key=nBTgfzmVkL2-N7DBMJ6e6gyk" alt="Branch-by-abstraction" width="1194" height="726" loading="lazy"></p>
<p>This is similar to how the strangler fig pattern works at an architectural level. It’s a bit of extra work (since you maintain two paths for a while), but it allows you to migrate functionality and fall back if needed incrementally.</p>
<p>Aim to identify the 20% of the code causing 80% of the problems. Focus your refactoring energy there for maximum impact. When you do, create a plan to isolate that area via abstractions, interfaces, modules, or other means so that you can work on it with minimal risk of side effects. The more you can contain the blast radius of a refactoring, the more confidently you can move forward.</p>
<h3 id="heading-2-incremental-vs-big-bang-refactoring">2. Incremental vs. Big Bang Refactoring</h3>
<p>One of the first strategic decisions is approaching the refactor <strong>incrementally</strong> or going for a <strong>“big bang”</strong> overhaul. In most cases, an incremental approach is preferable, but there are scenarios where more significant coordinated refactoring steps are considered.</p>
<p><strong>Let’s break down what these mean:</strong></p>
<pre><code class="lang-python"><span class="hljs-comment"># before: one large function with multiple responsibilities</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">process_order</span>(<span class="hljs-params">order</span>):</span>
    validate(order)
    apply_discount(order)
    save_to_db(order)
    send_confirmation(order)
    log_metrics(order)
    update_loyalty_points(order)
    <span class="hljs-comment"># potentially more steps </span>

<span class="hljs-comment"># after: refactored incrementally into clearer, smaller units</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">process_order</span>(<span class="hljs-params">order</span>):</span>
    validate(order)
    apply_discount(order)
    persist_and_notify(order)

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">persist_and_notify</span>(<span class="hljs-params">order</span>):</span>
    save_to_db(order)
    send_confirmation(order)
    log_metrics(order)
    update_loyalty_points(order)
</code></pre>
<h4 id="heading-incremental-refactoring">Incremental refactoring</h4>
<p>This means making small, manageable changes over time rather than attempting a massive overhaul in one shot. The system should remain functional at each step (even internally in transition). The advantage is risk mitigation: each small change is less likely to go wrong, and it’s easier to pinpoint and fix if it does.</p>
<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXdaSmnIWRE9FKNmmABBzc6Tk6KFwsj29FQ2YwyQ_kWqryheb0yUdpec51lQHg5XahoxKgCm4vv9twD849H3Yo5dn0678tuGih9Z-HfBBCfhBngs4YhpH6x2pjzqnAeDVYGohXHvDQ?key=nBTgfzmVkL2-N7DBMJ6e6gyk" alt="Incremental refactoring" width="640" height="379" loading="lazy"></p>
<p>Incremental delivery lets you confirm changes in production and makes diagnosing issues easier since you’re only changing one small thing at a time​. It also means the system keeps running during the refactor, so there’s less pressure to rush to “get the system back to working condition”​. If priorities shift, you can pause after some increments and still have a working product.</p>
<h4 id="heading-big-bang-refactoring-rewrite">Big bang refactoring (Rewrite)</h4>
<p>This is the “tear it down and rebuild” approach. You stop adding new features, possibly freeze the code for a period, and devote a considerable effort to redesigning or rewriting a significant portion (or the entirety) of the system. The idea is to emerge on the other side with a <em>brand new, clean</em> system.</p>
<p>So when (if ever) is a big bang justified? Perhaps when the existing system is truly untenable – for example, an outdated technology that <strong>must</strong> be replaced (such as a platform that can’t meet new performance or security requirements or code written in a language no longer supported). Even then, wise teams often simulate a big bang by breaking it into stages or developing the new system in parallel.</p>
<p>Whenever possible, favor an incremental refactoring strategy. Teams successfully pull off massive transformations by treating the big refactor as a series of mini-refactors under a shared vision.</p>
<h3 id="heading-3-breaking-down-monolithic-code">3. Breaking Down Monolithic Code</h3>
<p>Many complex codebases start life as a single monolithic application, one deployable, a single code project, or a tightly coupled set of modules all maintained and released together.</p>
<p>Over time, monoliths can become unwieldy, builds take forever, a change in one area can unintentionally affect another, and teams can be complex to scale because everyone is stepping on each other’s toes in the same code. A common refactoring challenge for senior engineers is modularising or splitting a monolith into more manageable pieces.</p>
<pre><code class="lang-python"><span class="hljs-comment"># define the interface</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PaymentProcessor</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">charge</span>(<span class="hljs-params">self, amount</span>):</span> ...

<span class="hljs-comment"># old implementation</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LegacyProcessor</span>(<span class="hljs-params">PaymentProcessor</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">charge</span>(<span class="hljs-params">self, amount</span>):</span>
        <span class="hljs-comment"># original code</span>

<span class="hljs-comment"># new implementation behind a feature flag</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">NewProcessor</span>(<span class="hljs-params">PaymentProcessor</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">charge</span>(<span class="hljs-params">self, amount</span>):</span>
        <span class="hljs-comment"># cleaner code</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_processor</span>():</span>
    <span class="hljs-keyword">if</span> config.feature_new_payment:
        <span class="hljs-keyword">return</span> NewProcessor()
    <span class="hljs-keyword">return</span> LegacyProcessor()

<span class="hljs-comment"># usage remains the same</span>
processor = get_processor()
processor.charge(<span class="hljs-number">100</span>)
</code></pre>
<h4 id="heading-strategies-for-modularization">Strategies for modularization.</h4>
<ul>
<li><p><strong>Layer separation:</strong> Start by enforcing logical layer boundaries. For example, separate the user interface code from business logic and separate business logic from data access. In a messy monolith, these concerns often get mixed together. By organizing the code into layers (even within the same repository), you can limit the ripple effect of changes.</p>
</li>
<li><p><strong>Domain-based modularization:</strong> If your system spans multiple business domains or functional areas, consider splitting along those lines. For example, an e-commerce monolith might be separated into modules like Accounts, Orders, Products, Shipping, and so on.<br>  Each could become a subsystem or a package. The goal is to minimize the information these modules need to know about each other’s internals (high cohesion within modules and clear APIs between them).</p>
</li>
<li><p><strong>Microservices or services extraction:</strong> In recent years, the trend has been to break monoliths into microservices, independent services that communicate over APIs. This form of architectural refactoring can significantly improve independent deployability and scalability. But it’s a significant undertaking with complexities (distributed systems, network calls, and so on). If you decide to go this route, do it gradually.<br>  A proven method is the <strong>strangler fig pattern</strong> mentioned earlier: you pick one piece of functionality and rewrite or extract it as a separate service, redirect traffic or calls to the new service. At the same time, the rest of the monolith remains intact and iteratively does this for other pieces​.</p>
</li>
<li><p><strong>Modular monolith:</strong> Not every system needs to go full microservices. There’s an approach called a modular monolith, essentially structuring your single application into well-defined modules that communicate via explicit interfaces (almost like internal microservices but without the overhead of separate deployments).</p>
</li>
</ul>
<p>This can give you many microservices' advantages (clear boundaries, separate development responsibility) while avoiding operational complexity.</p>
<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXfLiNAEDyOsR4G_q1oQS3jpSenci3XDJRm10Gy3picTpaO9uHwme2H3YkbJF-Jrvqq3Q-QMxGjJJwy04mqUf1a7D8IRsCDER5pHBT6GTMPRkao5EXXIFGtj4Iki15mOHmRKRLTiWw?key=nBTgfzmVkL2-N7DBMJ6e6gyk" alt="microservices' advantages" width="1087" height="744" loading="lazy"></p>
<ul>
<li><strong>Identify shared utilities vs. truly independent components:</strong> In breaking down a monolith, some code is widely shared (like utility functions or cross-cutting concerns such as authentication). It might make sense to factor those into libraries or services <em>first</em>, as they will be needed by whatever other pieces you split out.</li>
</ul>
<p>While breaking down a monolith, maintaining functionality during the transition is essential. Techniques like backward compatibility (discussed next) and thorough testing will be your safety net.</p>
<p>Finally, be prepared for the team workflow to change. If you move to microservices, teams might take ownership of different services, requiring more DevOps and communication across teams. If you keep a modular monolith, enforce code ownership or review rules to keep the modules from tangling up again (for example, you might restrict direct database access from one module to another’s tables, and so on).</p>
<h3 id="heading-4-ensuring-backward-compatibility">4. Ensuring Backward Compatibility</h3>
<p>A critical concern during large refactoring is: <em>Will our changes break existing contracts</em>?</p>
<p>In other words, can other systems, modules, or clients that rely on our code work as expected after we refactor? Backward compatibility is especially important if your codebase provides public APIs (to external customers or other teams), data persisted in a certain format, configuration files that users have written, etc.</p>
<p>Here are some strategies and considerations to maintain backward compatibility:</p>
<p>Suppose you have a widely-used function like <code>send_email(to, subject, body)</code>. You want to refactor the internal logic to support additional features like HTML formatting, but you don’t want to break existing callers.</p>
<p>Instead of changing the function signature, you keep the public API unchanged and delegate to a new internal function:</p>
<pre><code class="lang-python"><span class="hljs-comment"># original API</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">send_email</span>(<span class="hljs-params">to, subject, body</span>):</span>
    <span class="hljs-comment"># send mail...</span>

<span class="hljs-comment"># refactored internals, keep signature</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">send_email</span>(<span class="hljs-params">to, subject, body</span>):</span>
    sendv2(to=to, subject=subject, body=body)

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">sendv2</span>(<span class="hljs-params">to, subject, body, html=True</span>):</span>
    <span class="hljs-comment"># new implementation with HTML support</span>
</code></pre>
<p>The internal <code>send_email_v2()</code> function adds new capabilities like HTML formatting, but older code using <code>send_email()</code> still works without any modifications.</p>
<p>If you're introducing a new, improved version like <code>send_email_v2(to, subject, body, html=True)</code>, it's good practice to:</p>
<ul>
<li><p>Mark the old version (send_email) as deprecated in documentation.</p>
</li>
<li><p>Ensure the old version internally calls the new one.</p>
</li>
<li><p>Give other teams time to migrate at their own pace.</p>
</li>
</ul>
<h4 id="heading-use-versioning-for-external-apis">Use versioning for external APIs</h4>
<p>If your system provides an HTTP API or similar to external clients, the safest route for major changes is to version the API. Introduce a v2 API endpoint for the refactored logic, keep v1 running (maybe internally calling v2 or using a translation layer). Clients can move to v2 at their own pace.</p>
<p>It’s extra work to maintain two APIs temporarily, but it prevents a breaking change from angering users or causing outages. Always communicate changes clearly and provide migration guides if applicable.</p>
<h4 id="heading-have-a-clear-deprecation-policy">Have a clear deprecation policy</h4>
<p>Make sure there’s a policy (and communication) around how long deprecated features will be supported. For internal APIs, maybe it’s one release cycle. For external ones, maybe multiple cycles or never removal without a major version bump. A good practice is to announce deprecation early.</p>
<p>If you’re exposing an HTTP API, consider introducing a new versioned endpoint (for example, <strong>/api/v2/send_email</strong>) and maintain the older <strong>/api/v1/send_email temporarily</strong>. Internally, v1 might call v2 with default parameters, ensuring behavior stays consistent for existing clients.</p>
<p>In summary, maintain backward compatibility whenever possible, and implement a clear deprecation policy for anything you do change​.</p>
<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXe3xM4som_GQrtHXI3NNR0G-4KJ-1D2YO-JbNdT75IxZ5_upcBRDnOVp7krEESiqwwtXg18pDypLq3VxDr44Hof76cs8HajOZy2w0FZ50kWmPk6Y7EwNByNLNrqAokmhmmL5sP3AA?key=nBTgfzmVkL2-N7DBMJ6e6gyk" alt="Clear deprecation policy" width="768" height="493" loading="lazy"></p>
<h4 id="heading-write-adapter-or-compatibility-layers">Write adapter or compatibility layers</h4>
<p>In some cases, you can write an adapter to bridge old and new systems. For instance, suppose you refactor the underlying data model of your application, but you still have old configuration files in the old format. Rather than forcing all those files to be rewritten immediately, you could write a small adapter that translates the old format to the new one at runtime (or during startup). This way, old data continues to work. </p>
<h4 id="heading-test-for-compatibility">Test for compatibility</h4>
<p>Include tests that specifically ensure backward compatibility. For instance, if you have a public API, keep a suite of tests using the old API contracts and run them against the refactored code, they should still pass. </p>
<p>In summary, ensure that as you refactor, the external behavior and contracts remain consistent. This careful approach protects your users and downstream systems, allowing you to reap the internal benefits of refactoring without causing external chaos.</p>
<h3 id="heading-5-handling-dependencies-and-tight-coupling">5. Handling dependencies and tight coupling</h3>
<p>One of the hairiest aspects of refactoring a large codebase is dealing with deeply interdependent code. Complex systems often suffer from tight coupling. Module A assumes details about Module B and vice versa, global variables or singletons are used all over, or a change in one place ripples through half the codebase.</p>
<p>Reducing coupling is a significant aim of refactoring because it makes the code more modular, meaning each piece can be understood, tested, and changed independently. So, how do we gradually loosen the coupling in a legacy system?</p>
<p>Let’s go over some strategies to reduce coupling.</p>
<h4 id="heading-introduce-interfaces-or-abstraction-layers">Introduce interfaces or abstraction layers</h4>
<p>A very effective way to decouple is to put an interface between components. For example, if you have a class that directly queries a database, introduce an interface and have the class use that instead. The underlying database code implements the interface.</p>
<pre><code class="lang-python"><span class="hljs-comment"># before: direct instantiation</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderService</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self</span>):</span>
        self.repo = OrderRepository()

<span class="hljs-comment"># after: inject dependency</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderService</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, repo</span>):</span>
        self.repo = repo

<span class="hljs-comment"># wiring up in application startup</span>
repo = OrderRepository(db_conn)
service = OrderService(repo)
</code></pre>
<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXfuMNvzC4x3X0EOgoRXzflfOv4C-Dxzc2Tm16KA0NdZcOH0nK300LUwcNzXCL6iqu0rhknHiVhnQN4csDCYUupQLc4Kt6Q4c7d1Pi47NfrXKoF9rhXCUMAhtozsDpFMVT2lo2OX5Q?key=nBTgfzmVkL2-N7DBMJ6e6gyk" alt="Introduce interfaces or abstraction layers" width="1600" height="441" loading="lazy"></p>
<p>Now, that class no longer depends on how the data is fetched. Applying the dependency inversion principle depends on abstractions, not concretions.</p>
<h4 id="heading-use-dependency-injection">Use dependency injection</h4>
<p>Once you have interfaces, use dependency injection to supply concrete implementations. Many frameworks support DI containers, or you can do it manually (passing in dependencies via constructors). Dependency injection means code A doesn’t instantiate code B itself – instead, B is passed into A.  </p>
<p>This approach also makes unit testing easier (you can inject mock dependencies).</p>
<h4 id="heading-facades-or-wrapper-services">Facades or wrapper services</h4>
<p>If a particular subsystem is heavily entangled with others, consider creating a Facade, an object that provides a simplified interface to a larger body of code. Other parts of the system are then called the Facade, not the many internal methods of the subsystem. Internally, the subsystem can be refactored (even split into smaller pieces) as long as the Facade’s outward interface remains consistent.</p>
<p>This is similar to how microservices work (other services don’t care how one service is implemented internally – they just call its API), but you can do it in-process, too.</p>
<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXe_X2G_VNTR-I2EIp86SgPD3Zlks70Q4iG3BsqIs94PMgh-_qNfRk7ogT4mqONP7qXzg8PpN92k342-2nH6ertfy32Ga6SFH3PdSLwxP4US9PPjMi6Rqc9hy-gHbSKVzvTvYmTzOQ?key=nBTgfzmVkL2-N7DBMJ6e6gyk" alt="Facades or wrapper services" width="1600" height="1549" loading="lazy"></p>
<h4 id="heading-gradual-replacement-parallel-run">Gradual replacement (Parallel Run)</h4>
<p>If a specific component is to be replaced with a new implementation, it can help to run them in parallel for a while. For instance, if you have a spaghetti module that you want to redo correctly, you could leave the spaghetti code in place for legacy calls but start routing new calls to the new module.</p>
<p>The result is a codebase where changes in one area (hopefully) won’t unpredictably break another, a key property of a maintainable system.</p>
<h3 id="heading-6-testing-strategies-safely-refactoring-with-confidence">6. Testing Strategies (Safely Refactoring with Confidence)</h3>
<p>A robust testing strategy will give you the confidence to make sweeping changes because you’ll know quickly if something important breaks. Here’s how to approach testing in the context of a large refactoring:</p>
<h4 id="heading-establish-a-baseline-with-regression-tests">Establish a baseline with regression tests</h4>
<p>Before you even begin refactoring a particular component, make sure you have tests that cover its current behavior. You're lucky if the codebase already has a good test suite, but many legacy systems have inadequate tests.</p>
<p>One of the first tasks in those cases is often writing <strong>characterization tests</strong>. A characterization test is a test that documents what the system <em>currently does</em>, not what we think it should do​.</p>
<p>As Feathers says, “a characterization test is a test that characterizes the actual behavior of a piece of code.” This allows you to take a snapshot of what it does and ensure that it doesn’t change​.</p>
<p>This gives you a safety net so you can refactor with confidence that you’re not introducing regressions​. Use automated test suites to help things run smoothly (unit, integration, end-to-end).</p>
<h4 id="heading-continuous-integration-ci">Continuous integration (CI)</h4>
<p>It is highly recommended that testing be integrated into a CI pipeline that runs on every commit or merge. This way, you catch a bug during refactoring as soon as you introduce it, tightening the feedback loop.</p>
<h4 id="heading-canary-releases-and-feature-flags">Canary releases and feature flags</h4>
<p>Beyond pre-release testing, consider strategies for safely deploying refactored code. A canary release involves rolling out the change to a small subset of users or servers first, observing it, and then gradually expanding​.</p>
<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXfAif0ftiqEhiRPDygrmhtzSsfrctq6ZPfJnMg04GwKmxKk-NFiP9GjEGE9rfz7U_WKhRcBYSBYlirjKwzr-PvfZz2FJpEWS6U0UqNh-WayiVM5BGIyz3sabSX-zdKKA0j_ojvhIA?key=nBTgfzmVkL2-N7DBMJ6e6gyk" alt="Canary releases and feature flags" width="593" height="210" loading="lazy"></p>
<p>This is great for catching issues that tests might miss (for example, performance issues or edge cases in production data). If the canary looks good (no errors, metrics are healthy), you proceed to full rollout. If not, you rollback quickly—with only a small impact scope.</p>
<h4 id="heading-performance-and-load-testing">Performance and load testing</h4>
<p>If performance is a concern, incorporate performance tests into your strategy. This can be done in a staging environment. You might reconsider your approach or optimize the new code if you see a significant regression.</p>
<h4 id="heading-testing-legacy-code-lacking-tests">Testing legacy code lacking tests</h4>
<p>If you’re dealing with a part of the system with zero tests (not uncommon in older code), prioritize getting at least some coverage there. There are also techniques like <strong>approval testing</strong> (where you generate output and have a human approve it as correct, then use that as a baseline for future tests). The key is not to refactor entirely in the dark; give yourself at least a flashlight in the form of tests!</p>
<p>In sum, a strong testing strategy is non-negotiable for refactoring complex systems. It’s your safety net, early warning system, and guide to know that your “cleanup” hasn’t broken anything vital.</p>
<h3 id="heading-7-refactoring-without-breaking-performance">7. Refactoring Without Breaking Performance</h3>
<p>A common concern when refactoring is whether these cleaner code changes will make my system slower or more resource-hungry. Ideally, refactoring is about the internal structure and shouldn’t change external behavior, and performance is part of the behavior.</p>
<p>In theory, performance should remain the same if you don’t change algorithms or data structures in a way that affects complexity.</p>
<p>In practice, though, performance can be inadvertently affected by refactoring. The new code may be more readable but uses more memory, or perhaps a critical caching mechanism was removed in the spirit of simplicity.</p>
<p><strong>Senior engineers need to be mindful of performance-sensitive parts of the system when refactoring and take steps to avoid regressions (or even improve performance where possible).</strong></p>
<p>Here’s how to refactor with performance in mind:</p>
<h4 id="heading-identify-performance-critical-code-paths">Identify performance-critical code paths</h4>
<p>Not all codes are equal regarding performance impact. If you refactor them, treat it almost like a functional change: you must re-measure performance afterwards. You have more leeway for parts of the code that run rarely or are not bottlenecks.</p>
<h4 id="heading-use-profiling-before-and-after">Use profiling before and after</h4>
<p>A profiler is a tool that measures where time is spent in your code or how memory is allocated. It’s beneficial to run a profiler on the code before refactoring a module to see how it behaves, and then run it after to compare. If you see, for example, that after refactoring, a function now shows up as taking 30% of execution time (when it was negligible before), that’s a red flag. Maybe the new code calls it more times than before.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> cProfile, pstats
<span class="hljs-keyword">from</span> mymodule <span class="hljs-keyword">import</span> slow_function

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">profile</span>(<span class="hljs-params">fn</span>):</span>
    profiler = cProfile.Profile()
    profiler.enable()
    fn()
    profiler.disable()
    stats = pstats.Stats(profiler).strip_dirs().sort_stats(<span class="hljs-string">'cumtime'</span>)
    stats.print_stats(<span class="hljs-number">10</span>)

<span class="hljs-comment"># run before refactor</span>
profile(<span class="hljs-keyword">lambda</span>: slow_function())

<span class="hljs-comment"># after you refactor slow_function(), re-run and compare stats</span>
</code></pre>
<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXd1xNcjypguN9JbN7JtBhAtBkfDrtCV6IwOORRUVT5rOAha_I2GQx3vgKRAjlxpeeUIGLTETRR6J3EnS2y95DY6ypiH95DQJT0vRfcyxv2KIz99hPXa0O8JjTzxpi5eSsk3spN6EQ?key=nBTgfzmVkL2-N7DBMJ6e6gyk" alt="profiler-dashboard" width="1464" height="1136" loading="lazy"></p>
<h4 id="heading-when-possible-improve-performance-through-refactoring">When possible, improve performance through refactoring</h4>
<p>On the flip side, refactoring can help performance.</p>
<p>For example, by refactoring duplicated code into one place, you can use better caching in that one place. So, watch for performance improvement opportunities that arise naturally as you refactor.</p>
<p>Performance should be treated as part of the “external behavior” that needs to be preserved in a good mindset. Refactoring should ideally not make things slower for users. To ensure that, incorporate performance checks into your plan, especially for critical sections. Measure, don’t guess. The end goal is a codebase that is both clean <strong>and</strong> fast enough.</p>
<h3 id="heading-8-automate-code-reviews-with-ai-tools">8. Automate Code Reviews with AI tools</h3>
<p>Refactoring code is an ongoing process, not a one-time event – AI code review tools help enforce clean-code standards, catch smells early, and reduce the repetitive tasks that can bog down human reviewers. This frees your engineers to focus on deeper architectural or domain-specific issues.</p>
<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXfWs-ZM80TK_JcjwyPEnywdJl6Tf4G6gYFa1cN_J2ugTlniaGr4a397JuUj721m7kUw0EKMnzYHykpHJdG_aW7w3_B2J91bLL1UoaabdNsmH1uckMJHcFVpAhqZM2r855AsVYwDJg?key=nBTgfzmVkL2-N7DBMJ6e6gyk" alt="CodeRabbit-ai-code-reviewer-tool" width="1600" height="779" loading="lazy"></p>
<p>One powerful option is <a target="_blank" href="https://www.coderabbit.ai/">CodeRabbit</a>, an AI-driven review platform designed to cut review time and bugs in half.</p>
<p>Here’s how it works and why it can boost your refactoring workflow:</p>
<h4 id="heading-ai-powered-contextual-feedback">AI-powered contextual feedback</h4>
<p>CodeRabbit analyzes pull requests line by line, applying both advanced language models and static analysis under the hood. It flags potential bugs, best-practice deviations, and style issues before a human opens the PR.</p>
<p>Some other features include:</p>
<ul>
<li><p><strong>Auto-generated summaries and 1-click fixes</strong> – Summarize large PRs and apply straightforward fixes instantly.</p>
</li>
<li><p><strong>Real-time collaboration and AI chat</strong> – Chat with the AI for clarifications, alternate code snippets, and instant feedback.</p>
</li>
<li><p><strong>Integrates with popular dev platforms</strong> – Supports GitHub, GitLab, and Azure DevOps for seamless PR scanning.</p>
</li>
</ul>
<p>CodeRabbit even has a free AI code reviews in VS Code and with this <a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=CodeRabbit.coderabbit-vscode">VS Code extension</a>, you can get the most advanced AI code reviews directly in your code editor, saving review time, catching more bugs, and helping you in refactoring.</p>
<h2 id="heading-summary">Summary</h2>
<p>Refactoring a complex enterprise codebase is like renovating a large building while people still live in it without collapsing the structure.</p>
<p>Refactoring should be an ongoing process. You prevent the codebase from decaying by incorporating these practices into your regular development (perhaps allocating some time each sprint for refactoring or doing it opportunistically when touching your code). Each minor refactoring should not be too complex, and the cumulative effect is significant.</p>
<p>As <a target="_blank" href="https://martinfowler.com/">Martin Fowler</a> puts it, a series of small changes can lead to a significant improvement in design.</p>
<p>That's it for this blog. I hope you learned something new today.</p>
<p>If you want to read more interesting articles about developer tools, React, Next.js, AI and more, then I'll encourage you to checkout my <a target="_blank" href="https://www.devtoolsacademy.com/">blog</a>.</p>
<p>Some of the new and interesting articles I've written in the last 24 months.</p>
<ul>
<li><p><a target="_blank" href="https://www.devtoolsacademy.com/blog/cursor-vs-windsurf/">Cursor vs Windsurf</a></p>
</li>
<li><p><a target="_blank" href="https://clerk.com/blog/nextjs-role-based-access-control">How to Implement Role-Based Access Control in Next.js</a></p>
</li>
<li><p><a target="_blank" href="https://www.devtoolsacademy.com/blog/ai-code-reviewers-vs-human-code-reviewers/">AI Code Reviewers vs Human Code Reviewers</a></p>
</li>
<li><p><a target="_blank" href="https://www.freecodecamp.org/news/how-i-built-a-custom-video-conferencing-app-with-stream-and-nextjs/">How to Build a Custom Video Conferencing App with Stream and Next.js</a></p>
</li>
<li><p><a target="_blank" href="https://www.freecodecamp.org/news/how-to-perform-code-reviews-in-tech-the-painless-way/">How to Perform Code Reviews in Tech – The Painless Way</a></p>
</li>
</ul>
<p>You can get in touch if you have any questions or corrections. I’m expecting them.</p>
<p>And if you found this blog useful, please share it with your friends and colleagues who might benefit from it as well. Your support enables me to continue producing useful content for the tech community.</p>
<p>Now it’s time to take the next step by subscribing to my <a target="_blank" href="https://bytesizedbets.com/"><strong>newsletter</strong></a> and following me on <a target="_blank" href="https://twitter.com/theankurtyagi"><strong>Twitter</strong></a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Refactor to Configurable Dependency in 5 Steps ]]>
                </title>
                <description>
                    <![CDATA[ By Bertil Muth Configurable Dependency, also known as Dependency Injection, is a pattern that enables you to switch dependencies of your application. The term was coined by Alistair Cockburn. Say your application has a GUI. But your administrator wan... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/refactoring-to-configurable-dependency-in-5-steps/</link>
                <guid isPermaLink="false">66d45dea4a7504b7409c335b</guid>
                
                    <category>
                        <![CDATA[ dependency injection ]]>
                    </category>
                
                    <category>
                        <![CDATA[ refactoring ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Fri, 19 Aug 2022 20:42:23 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2022/08/diana-polekhina-ONuLIzB0UtA-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Bertil Muth</p>
<p>Configurable Dependency, also known as Dependency Injection, is a pattern that enables you to switch dependencies of your application. The <a target="_blank" href="https://web.archive.org/web/20170624023207/http://alistair.cockburn.us/Configurable+Dependency">term</a> was coined by Alistair Cockburn.</p>
<p>Say your application has a GUI. But your administrator wants to use certain functions via the console. Or your production code calls an external service. But your tests shouldn’t call the service, since it doesn’t provide reliable results. Or the service isn’t always available.</p>
<p>Here’s where a Configurable Dependency is helpful. Depending on context, you use one dependency or the other.</p>
<p>A lot of articles attempt to explain the pattern. But they embed it in a broader context, like ports and adapters architecture. That makes understanding harder than necessary. I know, I’ve written such articles myself.</p>
<p>On top of that, many articles focus on greenfield applications. But most of us have to maintain applications that already exist.</p>
<p>Let’s start with a simple class that has a hard-wired dependency. Then we'll refactor it to a class that has a Configurable Dependency. </p>
<p>The example is trivial, but the refactoring steps are generally applicable to your own application if you’re in a similar situation.</p>
<p>I’ll refer to an example <a target="_blank" href="https://github.com/bertilmuth/configurable-dependency">GitHub project</a> in the article. It shows the steps to perform, in its commit history. At the end of the article, there are appendices for IntelliJ IDEA and Eclipse. They show how to do the refactoring steps in your IDE.</p>
<h1 id="heading-the-calculator-example">The Calculator Example</h1>
<p>Say you have a class called <code>Calculator</code>. It’s the equivalent of “business logic”. At the end of each calculation, it prints the result to the screen. In a real world application, it might save something to a database instead.</p>
<p><a target="_blank" href="https://github.com/bertilmuth/configurable-dependency/blob/3d8540dd9566cb48f29902b9c9e10292cf8f7560/src/main/java/example/Calculator.java">Here</a>’s the code:</p>
<pre><code class="lang-java"><span class="hljs-keyword">package</span> example;

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Calculator</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Calculator</span><span class="hljs-params">()</span></span>{
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">long</span> <span class="hljs-title">add</span><span class="hljs-params">(<span class="hljs-keyword">long</span> one, <span class="hljs-keyword">long</span> two)</span> </span>{
        <span class="hljs-keyword">long</span> result = one + two;
        printResult(result);
        <span class="hljs-keyword">return</span> result;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">long</span> <span class="hljs-title">sub</span><span class="hljs-params">(<span class="hljs-keyword">long</span> one, <span class="hljs-keyword">long</span> two)</span> </span>{
        <span class="hljs-keyword">long</span> result = one - two;
        printResult(result);
        <span class="hljs-keyword">return</span> result;
    }

    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">printResult</span><span class="hljs-params">(<span class="hljs-keyword">long</span> result)</span> </span>{
        System.out.println(<span class="hljs-string">"The result is: "</span> + result);
    }
}
</code></pre>
<p>Now if you want to test that class, your <a target="_blank" href="https://github.com/bertilmuth/configurable-dependency/blob/3d8540dd9566cb48f29902b9c9e10292cf8f7560/src/test/java/example/CalculatorTest.java">test code</a> may look like this:</p>
<pre><code class="lang-java"><span class="hljs-keyword">package</span> example;

<span class="hljs-keyword">import</span> <span class="hljs-keyword">static</span> org.junit.jupiter.api.Assertions.*;
<span class="hljs-keyword">import</span> org.junit.jupiter.api.*;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CalculatorTest</span> </span>{
    <span class="hljs-keyword">private</span> Calculator calculator;

    <span class="hljs-meta">@BeforeEach</span>
    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">setup</span><span class="hljs-params">()</span> </span>{
        calculator = <span class="hljs-keyword">new</span> Calculator();
    }

    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">addsToNumbers</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">long</span> result = calculator.add(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>);
        assertEquals(<span class="hljs-number">3</span>, result);
    }

    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">subtractsToNumbers</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">long</span> result = calculator.sub(<span class="hljs-number">5</span>, <span class="hljs-number">1</span>);
        assertEquals(<span class="hljs-number">4</span>, result);
    }
}
</code></pre>
<p>Every time you run your test, it prints the results to the screen. That’s unnecessary. It slows your tests down.</p>
<p>With dependency injection, you can still assert the result in the test. But you avoid printing the result to the screen (or saving the result to the database, the file system, or somewhere else).</p>
<h2 id="heading-step-1-move-dependency-creation-to-the-constructor">Step 1: Move Dependency Creation to the Constructor</h2>
<p>Find out where an instance of the dependent class is created. Move creation to the constructor by assigning the instance to a field. Use the field throughout the class, so that only the constructor creates the dependency.</p>
<p>The example code is a bit different. The dependency is a static one, <code>System.out</code>. But as described above, the refactored code assigns it to the <code>printer</code> field in the constructor.</p>
<p>So the <code>Calculator</code> class now looks like <a target="_blank" href="https://github.com/bertilmuth/configurable-dependency/blob/43ba441138edafdbca2f7f4a100f5e5b528235a2/src/main/java/example/Calculator.java">this</a>:</p>
<pre><code class="lang-java"><span class="hljs-keyword">package</span> example;

<span class="hljs-keyword">import</span> java.io.PrintStream;

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Calculator</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> PrintStream printer;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Calculator</span><span class="hljs-params">()</span></span>{
        printer = System.out;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">long</span> <span class="hljs-title">add</span><span class="hljs-params">(<span class="hljs-keyword">long</span> one, <span class="hljs-keyword">long</span> two)</span> </span>{
        <span class="hljs-keyword">long</span> result = one + two;
        printResult(result);
        <span class="hljs-keyword">return</span> result;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">long</span> <span class="hljs-title">sub</span><span class="hljs-params">(<span class="hljs-keyword">long</span> one, <span class="hljs-keyword">long</span> two)</span> </span>{
        <span class="hljs-keyword">long</span> result = one - two;
        printResult(result);
        <span class="hljs-keyword">return</span> result;
    }

    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">printResult</span><span class="hljs-params">(<span class="hljs-keyword">long</span> result)</span> </span>{
        printer.println(<span class="hljs-string">"The result is: "</span> + result);
    }
}
</code></pre>
<p>Run the test. It still needs to pass.</p>
<h2 id="heading-step-2-pass-the-dependency-as-a-constructor-argument">Step 2: Pass the Dependency as a Constructor Argument</h2>
<p>Pass the instance as a constructor argument, instead of creating it in the constructor. <a target="_blank" href="https://github.com/bertilmuth/configurable-dependency/blob/411cb1650ddfd209b12d0ebc9007fcd1f857d280/src/main/java/example/Calculator.java">Here</a>’s the refactored example code:</p>
<pre><code class="lang-java"><span class="hljs-keyword">package</span> example;

<span class="hljs-keyword">import</span> java.io.PrintStream;

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Calculator</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> PrintStream printer;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Calculator</span><span class="hljs-params">(PrintStream printer)</span></span>{
        <span class="hljs-keyword">this</span>.printer = printer;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">long</span> <span class="hljs-title">add</span><span class="hljs-params">(<span class="hljs-keyword">long</span> one, <span class="hljs-keyword">long</span> two)</span> </span>{
        <span class="hljs-keyword">long</span> result = one + two;
        printResult(result);
        <span class="hljs-keyword">return</span> result;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">long</span> <span class="hljs-title">sub</span><span class="hljs-params">(<span class="hljs-keyword">long</span> one, <span class="hljs-keyword">long</span> two)</span> </span>{
        <span class="hljs-keyword">long</span> result = one - two;
        printResult(result);
        <span class="hljs-keyword">return</span> result;
    }

    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">printResult</span><span class="hljs-params">(<span class="hljs-keyword">long</span> result)</span> </span>{
        printer.println(<span class="hljs-string">"The result is: "</span> + result);
    }
}
</code></pre>
<p>For this class to work, you need to adapt each line of code that creates a <code>Calculator</code> object to pass in the dependency. So the line to create the <code>Calculator</code> in <a target="_blank" href="https://github.com/bertilmuth/configurable-dependency/blob/411cb1650ddfd209b12d0ebc9007fcd1f857d280/src/test/java/example/CalculatorTest.java">CalculatorTest</a> now looks like this:</p>
<p><code>calculator = new Calculator(System.out);</code></p>
<p>Run the test. It still needs to pass.</p>
<h2 id="heading-step-3-create-an-interface-and-implementation">Step 3: Create an Interface and Implementation</h2>
<p>Create an <a target="_blank" href="https://github.com/bertilmuth/configurable-dependency/blob/d453d47d297eed94108f4b136e467fc66502fe84/src/main/java/example/PrintStream.java">interface</a> with the exact same name as the class name of the dependency. Place it in the same package. Put the method in it that the business logic calls.</p>
<pre><code class="lang-java"><span class="hljs-keyword">package</span> example;

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">PrintStream</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">println</span><span class="hljs-params">(String text)</span></span>;
}
</code></pre>
<p>In the <a target="_blank" href="https://github.com/bertilmuth/configurable-dependency/blob/d453d47d297eed94108f4b136e467fc66502fe84/src/main/java/example/Calculator.java">Calculator</a> class, remove the import statement of the dependency, <code>java.io.PrintStream</code>, to use the new interface instead.</p>
<p>Here’s one implementation of the interface. <a target="_blank" href="https://github.com/bertilmuth/configurable-dependency/blob/d453d47d297eed94108f4b136e467fc66502fe84/src/main/java/example/ConsolePrinter.java">ConsolePrinter</a> contains the original functionality that prints to a screen.</p>
<pre><code class="lang-java"><span class="hljs-keyword">package</span> example;

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ConsolePrinter</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">PrintStream</span></span>{
    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">println</span><span class="hljs-params">(String text)</span> </span>{
        System.out.println(text);
    }
}
</code></pre>
<p>In the <a target="_blank" href="https://github.com/bertilmuth/configurable-dependency/blob/d453d47d297eed94108f4b136e467fc66502fe84/src/test/java/example/CalculatorTest.java">test class</a>, pass in the <code>ConsolePrinter</code>. Run the test. It still needs to pass, and print the results to the screen:</p>
<pre><code class="lang-java"><span class="hljs-keyword">package</span> example;

<span class="hljs-keyword">import</span> <span class="hljs-keyword">static</span> org.junit.jupiter.api.Assertions.*;
<span class="hljs-keyword">import</span> org.junit.jupiter.api.*;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CalculatorTest</span> </span>{
    <span class="hljs-keyword">private</span> Calculator calculator;

    <span class="hljs-meta">@BeforeEach</span>
    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">setup</span><span class="hljs-params">()</span> </span>{
        calculator = <span class="hljs-keyword">new</span> Calculator(<span class="hljs-keyword">new</span> ConsolePrinter());
    }

    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">addsToNumbers</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">long</span> result = calculator.add(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>);
        assertEquals(<span class="hljs-number">3</span>, result);
    }

    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">subtractsToNumbers</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">long</span> result = calculator.sub(<span class="hljs-number">5</span>, <span class="hljs-number">1</span>);
        assertEquals(<span class="hljs-number">4</span>, result);
    }
}
</code></pre>
<h2 id="heading-step-4-rename-and-clean-up">Step 4: Rename and Clean Up</h2>
<p>This is an optional step. You can decide to rename the interface, to give it a more meaningful name. For example, rename <code>PrintStream</code> to <a target="_blank" href="https://github.com/bertilmuth/configurable-dependency/blob/d453d47d297eed94108f4b136e467fc66502fe84/src/test/java/example/CalculatorTest.java">Printer</a>. </p>
<p>You can also decide to move the class to a different package and rename its method(s).</p>
<p>Run the test. It still needs to pass.</p>
<h2 id="heading-step-5-configure-the-dependency">Step 5: Configure the Dependency</h2>
<p>Create <a target="_blank" href="https://github.com/bertilmuth/configurable-dependency/blob/66a4aadbaad42616661357bc0ba9f5aaa6f5d846/src/main/java/example/IdlePrinter.java">another implementation</a> that ignores the text argument, for testing purposes:</p>
<pre><code class="lang-java"><span class="hljs-keyword">package</span> example;

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">IdlePrinter</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Printer</span></span>{
    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">println</span><span class="hljs-params">(String text)</span> </span>{
        <span class="hljs-comment">// This is empty, because we don't want to print in tests.</span>
    }
}
</code></pre>
<p>Now, you can change a single line of the CalculatorTest to turn off printing:</p>
<p><code>calculator = new Calculator(new IdlePrinter());</code></p>
<p>Since Java 8, you don’t even need <code>IdlePrinter</code>. Pass in a Lambda function instead:</p>
<p><code>calculator = new Calculator(text -&gt; {});</code></p>
<h2 id="heading-congratulations">Congratulations!</h2>
<p>You have refactored to a Configurable Dependency.</p>
<p>Have a look at the <a target="_blank" href="https://github.com/bertilmuth/configurable-dependency">GitHub project</a> commit history to see the changes in code that I performed.</p>
<p>If you have any questions, leave a comment or contact me.</p>
<p>Twitter: <a target="_blank" href="https://twitter.com/BertilMuth">https://twitter.com/BertilMuth</a></p>
<p>LinkedIn: <a target="_blank" href="https://www.linkedin.com/in/bertilmuth/">https://www.linkedin.com/in/bertilmuth/</a></p>
<p>The following appendices explain the concrete refactoring steps in IntelliJ IDEA and Eclipse.</p>
<h1 id="heading-appendix-a-how-to-refactor-in-intellij-idea">Appendix A — How to refactor in IntelliJ IDEA</h1>
<h2 id="heading-intellij-step-1-move-dependency-creation-to-the-constructor">IntelliJ Step 1: Move Dependency Creation to the Constructor</h2>
<p>Open the <code>Calculator</code> class. Locate <code>System.out</code>. Right click, select <code>Refactor &gt; Introduce Field</code>. Select <code>initialize in: constructor</code>. Name the field <code>printer</code>. Hit Return.</p>
<h2 id="heading-intellij-step-2-pass-the-dependency-as-a-constructor-argument">IntelliJ Step 2: Pass the Dependency as a Constructor Argument</h2>
<p>Set the cursor into the constructor, <code>Calculator()</code>. Mark the access to the dependency, <code>System.out</code>. Right click, select <code>Refactor &gt; Introduce Parameter</code>. Name the field <code>printer</code>. Hit Return.</p>
<h2 id="heading-intellij-step-3-create-an-interface-and-implementation">IntelliJ Step 3: Create an interface and Implementation</h2>
<p>Create the interface and implementation in the same package:</p>
<pre><code class="lang-java"><span class="hljs-keyword">package</span> example;

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">PrintStream</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">println</span><span class="hljs-params">(String text)</span></span>;
}
</code></pre>
<pre><code class="lang-java"><span class="hljs-keyword">package</span> example;

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ConsolePrinter</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">PrintStream</span></span>{
    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">println</span><span class="hljs-params">(String text)</span> </span>{
        System.out.println(text);
    }
}
</code></pre>
<p>Go to the business logic class <code>Calculator</code> and remove the import statement <code>import java.io.PrintStream;</code>. Save the file.</p>
<p>In the test class <code>CalculatorTest</code>, use the new implementation class.</p>
<p>Change this:</p>
<p><code>calculator = new Calculator(System.out);</code></p>
<p>to this:</p>
<p><code>calculator = new Calculator(new ConsolePrinter());</code></p>
<p>Save the file. Run the test and check if it still passes.</p>
<h2 id="heading-intellij-step-4-rename-and-clean-up">IntelliJ Step 4: Rename and Clean Up</h2>
<p>Go to the interface <code>PrintStream</code>. Right click on <code>PrintStream</code> and select <code>Refactor &gt; Rename</code>. Enter <code>Printer</code> and press Return. Run the test and check if it still passes.</p>
<h2 id="heading-intellij-step-5-configure-the-dependency">IntelliJ Step 5: Configure the Dependency</h2>
<p>Create another implementation that ignores the text argument, for testing purposes:</p>
<pre><code class="lang-java"><span class="hljs-keyword">package</span> example;

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">IdlePrinter</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Printer</span></span>{
    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">println</span><span class="hljs-params">(String text)</span> </span>{
        <span class="hljs-comment">// This is empty, because we don't want to print in tests.</span>
    }
}
</code></pre>
<p>Go to <code>CalculatorTest</code> and change this:</p>
<p><code>calculator = new Calculator(new ConsolePrinter());</code></p>
<p>to this:</p>
<p><code>calculator = new Calculator(new IdlePrinter());</code></p>
<p>Save the file. Run the test and check if it still passes.</p>
<p>The text output should now be off. Well done.</p>
<h1 id="heading-appendix-b-how-to-refactor-in-eclipse">Appendix B — How to Refactor in Eclipse</h1>
<h2 id="heading-eclipse-step-1-move-dependency-creation-to-the-constructor">Eclipse Step 1: Move Dependency Creation to the Constructor</h2>
<p>Open the <code>Calculator</code> class. Locate <code>System.out</code>. Right click, select <code>Refactor &gt; Extract Local Variable</code>. Click <code>OK</code>. </p>
<p>Mark the newly created local variable <code>out</code>. Right click, select <code>Refactor &gt; Convert Local Variable to Field</code>. Name the variable <code>printer</code>. Choose <code>Initialize in Class Constructors</code>. Check <code>Declare field as ‘final’</code>. Hit <code>OK</code>.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/08/grafik-13.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Run the test and check if it still passes.</p>
<h2 id="heading-eclipse-step-2-pass-the-dependency-as-a-constructor-argument">Eclipse Step 2: Pass the Dependency as a Constructor Argument</h2>
<p>Set the cursor into the constructor, <code>Calculator()</code>. Mark the access to the dependency, <code>System.out</code>, and copy it to the clipboard (CTRL-C).</p>
<p>Right click, select <code>Refactor &gt; Change Method Signature</code>. Click <code>Add</code>. Type <code>PrintStream</code> under <code>Type</code>, <code>printer</code> under <code>Name</code>, and paste <code>System.out</code> under <code>Default value.</code> Click <code>OK</code> and <code>Continue.</code></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/08/grafik-14.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Change the constructor to look like this:</p>
<p><code>public Calculator(PrintStream printer){</code><br> <code>this.printer = printer;</code><br> <code>}</code></p>
<p>Save the file. Run the test and check if it still passes.</p>
<h2 id="heading-eclipse-step-3-create-an-interface-and-implementation">Eclipse Step 3: Create an Interface and Implementation</h2>
<p>Create the interface and implementation in the same package:</p>
<pre><code class="lang-java"><span class="hljs-keyword">package</span> example;

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">PrintStream</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">println</span><span class="hljs-params">(String text)</span></span>;
}
</code></pre>
<pre><code class="lang-java"><span class="hljs-keyword">package</span> example;

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ConsolePrinter</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">PrintStream</span></span>{
    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">println</span><span class="hljs-params">(String text)</span> </span>{
        System.out.println(text);
    }
}
</code></pre>
<p>Go to the business logic class <code>Calculator</code> and remove the import statement <code>import java.io.PrintStream;</code>. Save the file.</p>
<p>In the test class <code>CalculatorTest</code>, use the new implementation class.</p>
<p>Change this:</p>
<p><code>calculator = new Calculator(System.out);</code></p>
<p>to this:</p>
<p><code>calculator = new Calculator(new ConsolePrinter());</code></p>
<p>Save the file. Run the test and check if it still passes.</p>
<h2 id="heading-eclipse-step-4-rename-and-clean-up">Eclipse Step 4: Rename and Clean Up</h2>
<p>Go to the interface <code>PrintStream</code>. Right click on <code>PrintStream</code> and select <code>Refactor &gt; Rename</code>. Enter <code>Printer</code> and press Return. Run the test and check if it still passes.</p>
<h2 id="heading-eclipse-step-5-configure-the-dependency">Eclipse Step 5: Configure the Dependency</h2>
<p>Create another implementation that ignores the text argument, for testing purposes:</p>
<pre><code class="lang-java"><span class="hljs-keyword">package</span> example;

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">IdlePrinter</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Printer</span></span>{
    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">println</span><span class="hljs-params">(String text)</span> </span>{
        <span class="hljs-comment">// This is empty, because we don't want to print in tests.</span>
    }
}
</code></pre>
<p>Go to <code>CalculatorTest</code> and change this:</p>
<p><code>calculator = new Calculator(new ConsolePrinter());</code></p>
<p>to this:</p>
<p><code>calculator = new Calculator(new IdlePrinter());</code></p>
<p>Save the file. Run the test and check if it still passes.</p>
<p>The text output should now be off. Well done.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Code Refactoring Best Practices – with Python Examples ]]>
                </title>
                <description>
                    <![CDATA[ In this article, I will discuss what code refactoring is. We'll go over why you need to refactor your code, the advantages of code refactoring, and some best practices for code refactoring. What is Code Refactoring? Refactoring means organizing your ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/best-practices-for-refactoring-code/</link>
                <guid isPermaLink="false">66bae7cf02a6638884966f13</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ refactoring ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ David Fagbuyiro ]]>
                </dc:creator>
                <pubDate>Thu, 18 Aug 2022 15:15:29 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2022/08/Author-Share-ImageFREE--1-.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this article, I will discuss what code refactoring is. We'll go over why you need to refactor your code, the advantages of code refactoring, and some best practices for code refactoring.</p>
<h2 id="heading-what-is-code-refactoring">What is Code Refactoring?</h2>
<p>Refactoring means organizing your code without modifying its original functionality. Refactoring is the process of making small changes or adjustments to your code without affecting or changing how the code functions while in use.</p>
<h2 id="heading-why-do-you-need-to-refactor-your-code">Why Do You Need to Refactor Your Code?</h2>
<h3 id="heading-it-helps-improve-your-codeprogram">It Helps Improve Your Code/Program</h3>
<p>Refactoring helps you improve the design of your software because developing applications is a difficult task. As you add additional functions, the relevance of the program may be affected. Then, abstractions are no longer as pure as they once were. </p>
<p>You can improve things by refactoring your code regularly to demonstrate a better understanding of the program or code you're working with.</p>
<h3 id="heading-it-gives-you-a-better-understanding-of-the-code">It Gives You a Better Understanding of the Code</h3>
<p>If you refactor properly, your code will be more easily understandable afterwards. You can also make the code easier to understand by improving the design. </p>
<p>It is common knowledge that developers read code far more frequently than they write it. As a result, it's in your best interest to keep things as simple as possible, which considerably improves maintainability. Also, people who read it in the future will be grateful.</p>
<h3 id="heading-refactoring-broadens-your-knowledge">Refactoring Broadens Your Knowledge</h3>
<p>Finally, refactoring is a method of active learning. Even if you didn't write the code, refactoring it gives you a better grasp of what it does.</p>
<h2 id="heading-how-refactoring-helps-your-team">How Refactoring Helps Your Team</h2>
<p>Refactoring is a common activity for software development teams. If done correctly, refactoring your code makes it easier for your team members to understand your code and the changes you've made.</p>
<p>The refactoring stage is often difficult for teams to include in their process. But refactoring, in my opinion, is an essential component of working on any process. Adding comments for me entails leaving the code in a better state than when I found it. </p>
<p>Whatever you do, avoid creating a separation between technical value and business value (creating the feature). You can do this by keeping the code understandable.</p>
<h2 id="heading-refactoring-best-practices-with-python-examples">Refactoring Best Practices – with Python Examples</h2>
<p>If you want to get the most benefit from restructuring your code, you should keep the following in mind:</p>
<ul>
<li>Refactor only tested code. Without tests, refactoring code is, to put it bluntly, gambling. If you don't have tests that verify that the before and after are equivalent, you can't be certain you didn't break the code. No matter how disciplined you are, eventually, you'll introduce bugs.</li>
<li>Minimize refactoring. The modification is riskier the more drastic it is. Divide it into several more compact portions. You get flexibility and mitigation.</li>
<li>Take little, gradual steps. Check to see if everything is working after making a small code change. </li>
<li>Observe the guidelines. Don't begin adding functionality while refactoring is still being done. Refactoring has no impact on the system's output. Before moving on to the next challenge, complete this one.</li>
</ul>
<p>Below is an example of code refactoring using the <strong>Define function</strong> in Python to find the force (F) required to move an object of mass (m) with an acceleration (a) given by the formula “F = m x a”. </p>
<p>Because we are performing a repeated task, it's a good idea to use a function instead of using only the conditional statement. So when we need to in the future, we just have to call the function out for usage.</p>
<h3 id="heading-code-before-refactoring">Code Before Refactoring:</h3>
<table><colgroup></colgroup><tbody><tr><td><p><span>#Using the Force formula F=ma</span><span><br></span><span><br></span><span>while</span><span> </span><span>True</span><span>:</span><span><br></span><span>&nbsp; mass = int(input(</span><span>"Enter the mass value: "</span><span>))</span><span><br></span><span>&nbsp; </span><span>if</span><span> mass &gt; </span><span>0</span><span>:</span><span><br></span><span>&nbsp; &nbsp; </span><span>break</span><span><br></span><span>while</span><span> </span><span>True</span><span>:</span><span><br></span><span>&nbsp; acceleration = int(input(</span><span>"Enter the acceleration: "</span><span>))</span><span><br></span><span>&nbsp; </span><span>if</span><span> acceleration &gt; </span><span>0</span><span>:</span><span><br></span><span>&nbsp; &nbsp; </span><span>break</span><span><br></span><span>print(</span><span>"The Force is"</span><span>, mass * acceleration)</span></p></td></tr></tbody></table>

<p>Notice how the two blocks look nearly identical. What if we’re calculating the “Work done” by a person or machine? Most likely, we’d copy and paste the while loop, changing the variable name and input prompt. Now let's use a function to refactor this.</p>
<h3 id="heading-code-after-refactoring">Code After Refactoring:</h3>
<table><colgroup></colgroup><tbody><tr><td><p><span>#REFACTORED code</span><span><br></span><span><br></span><span>def</span><span> </span><span>input_positive_integer</span><span>(prompt)</span><span>:</span><span><br></span><span>&nbsp; </span><span>while</span><span> </span><span>True</span><span>:</span><span><br></span><span>&nbsp; &nbsp; input_value = int(input(prompt))</span><span><br></span><span>&nbsp; &nbsp; </span><span>if</span><span> input_value &gt; </span><span>0</span><span>:</span><span><br></span><span>&nbsp; &nbsp; &nbsp; </span><span>return</span><span> input_value</span><span><br></span><span>mass = input_positive_integer(</span><span>"Enter the mass: "</span><span>)</span><span><br></span><span>acceleration = input_positive_integer(</span><span>"Enter the acceleration: "</span><span>)</span><span><br></span><span>print(</span><span>"The Force is"</span><span>, mass * acceleration)</span><span><br><br></span></p></td></tr></tbody></table>

<p>Now that our code is simpler to understand, we can start to save some lines of code. What's more, in the future, we'll be able to effortlessly insert our function into another script by just calling out the function, saving us time.</p>
<p>Here are some other tips to keep in mind while refactoring your Python code:</p>
<h3 id="heading-use-list-comprehension-instead-of-a-for-loop">Use List Comprehension Instead of a For Loop</h3>
<p>One of the most well-known Python constructs is list comprehension. Compared to several basic for loops, list comprehension provides a cleaner, quicker solution.</p>
<p>The code will run more quickly, existing data won't be altered, and list comprehensions are simpler to understand at a glance once you become used to them. </p>
<p>Below is a simple expression of using list comprehension instead of a For Loop:</p>
<p>Initial code:</p>
<table><colgroup></colgroup><tbody><tr><td><p><span>#List Comprehension instead of For Loop</span><span><br></span><span><br></span><span>numbers = [1,2,3,4,5,6,7,8]</span><span><br></span><span>odd_numbers = []</span><span><br></span><span>for</span><span> item </span><span>in</span><span> numbers:</span><span><br></span><span>&nbsp; </span><span>if</span><span> item % </span><span>2</span><span> == </span><span>1</span><span>:</span><span><br></span><span>&nbsp; &nbsp; odd_numbers.append(item)</span><span><br></span><span>print(odd_numbers)</span><span><br></span><span><br></span><span>#Outcome will be [1,3,5,7]</span></p></td></tr></tbody></table>

<p>In the above example, we loop through a list of integers (numbers) to see if we should enter each one into the odd numbers list.</p>
<p>Refactored code:</p>
<table><colgroup></colgroup><tbody><tr><td><p><span>numbers = [</span><span>1</span><span>,</span><span>2</span><span>,</span><span>3</span><span>,</span><span>4</span><span>,</span><span>5</span><span>,</span><span>6</span><span>,</span><span>7</span><span>,</span><span>8</span><span>]</span><span><br></span><span>odd_numbers = [item </span><span>for</span><span> item </span><span>in</span><span> numbers </span><span>if</span><span> item % </span><span>2</span><span> == </span><span>1</span><span>]</span><span><br></span><span>print</span><span> (odd_numbers)</span></p></td></tr></tbody></table>

<p>In the above-refactored code, the list comprehension is an assignment into the odd numbers. Then we iterate over the "numbers" and set the return (the first item). Then, we can include an if-statement to filter.</p>
<h3 id="heading-use-def-functions-for-repetitive-tasks">Use Def Functions for Repetitive Tasks</h3>
<p>Defining functions for repetitive tasks is the first and perhaps most significant refactoring we can perform. </p>
<p>Although functions provide many advantages, from the standpoint of refactoring, we will shorten our code and increase readability. In addition, you only have to update the job once, in the declaration of the function (the example given below).</p>
<p>Initial code:</p>
<table><colgroup></colgroup><tbody><tr><td><p><span>#Using the Work done expression W=f<em>d</em></span><span><br></span><span><br></span><span>while</span><span> </span><span>True</span><span>:</span><span><br></span><span>&nbsp; force = int(input(</span><span>"Enter the force value:&nbsp; "</span><span>))</span><span><br></span><span>&nbsp; </span><span>if</span><span> force &gt; </span><span>0</span><span>:</span><span><br></span><span>&nbsp; &nbsp; </span><span>break</span><span><br></span><span>while</span><span> </span><span>True</span><span>:</span><span><br></span><span>&nbsp; distance = int(input(</span><span>"Enter the distance value:&nbsp; "</span><span>))</span><span><br></span><span>&nbsp; </span><span>if</span><span> distance &gt; </span><span>0</span><span>:</span><span><br></span><span>&nbsp; &nbsp; </span><span>break</span><span><br></span><span>print(</span><span>"The Workdone is"</span><span>, force  distance)</span></p></td></tr></tbody></table>

<p>Refactored code:</p>
<table><colgroup></colgroup><tbody><tr><td><p><span>#REFACTORED code</span><span><br></span><span><br></span><span>def</span><span> </span><span>input_positive_integer</span><span>(prompt)</span><span>:</span><span><br></span><span>&nbsp; </span><span>while</span><span> </span><span>True</span><span>:</span><span><br></span><span>&nbsp; &nbsp; input_value = int(input(prompt))</span><span><br></span><span>&nbsp; &nbsp; </span><span>if</span><span> input_value &gt; </span><span>0</span><span>:</span><span><br></span><span>&nbsp; &nbsp; &nbsp; </span><span>return</span><span> input_value</span><span><br></span><span>force = input_positive_integer(</span><span>"Enter the force value:&nbsp; "</span><span>)</span><span><br></span><span>distance = input_positive_integer(</span><span>"Enter the distance:&nbsp; "</span><span>)</span><span><br></span><span>print(</span><span>"The Workdone is"</span><span>, force * distance, </span><span>"J"</span><span>)</span><span><br><br></span></p></td></tr></tbody></table>

<h3 id="heading-use-ternary-operators-instead-of-if-else-statements">Use Ternary Operators Instead of if-else Statements:</h3>
<p>You can use an easy if-statement like this to find if an input integer is an "even" or "odd" number instead of an if-else statement:</p>
<table><colgroup></colgroup><tbody><tr><td><p><span># USING TERNARY OPERATOR</span><span><br></span><span><br></span><span>to_check = int(input(</span><span>"Enter a whole number :"</span><span>))</span><span><br></span><span>msg = </span><span>"Even"</span><span> </span><span>if</span><span> to_check%</span><span>2</span><span> == </span><span>0</span><span> </span><span>else</span><span> </span><span>"Odd"</span><span><br></span><span>print(</span><span>"The number you entered is"</span><span>, msg, </span><span>"number"</span><span>)</span><span><br></span><span><br></span><span><br></span><span># USING USUAL IF-ELSE</span><span><br></span><span><br></span><span>to_check = int(input(</span><span>"Enter a whole number :"</span><span>))</span><span><br></span><span>msg = </span><span>""</span><span><br></span><span>if</span><span>(to_check%</span><span>2</span><span> == </span><span>0</span><span>):</span><span><br></span><span>&nbsp; msg = </span><span>"Even"</span><span><br></span><span>else</span><span>:</span><span><br></span><span>&nbsp; msg = </span><span>"Odd"</span><span><br></span><span>print(</span><span>"The number you entered is"</span><span>, msg, </span><span>"number"</span><span>)</span><span><br></span><span><br><br></span></p></td></tr></tbody></table>

<p>The ternary operator is used in the code above to determine if an integer is even or odd.</p>
<p><code>msg</code> will be assigned <code>Even</code> if the condition (to_check% 2 == 0) is true. <code>msg</code> will also be assign <code>Odd</code> if the condition (to check% 2 == 0) is not true.</p>
<p>Note: you can't use the ternary operator to replace any code that uses an if-else expression. For example, if-else statements involving more than two cases.</p>
<h3 id="heading-use-the-enumerate-function-instead-of-range">Use the Enumerate Function Instead of Range():</h3>
<p>Python for loops can be complicated, especially if you are coming from another programming language. </p>
<p>Initially, you might have thought to make an index variable and use it to loop through your list. As a result, people often find and utilize the range() method. But it's not a best practice to use this method. Below is an example:</p>
<p>Code Before Refactoring:</p>
<table><colgroup></colgroup><tbody><tr><td><p><span>colors = [</span><span>"white"</span><span>, </span><span>"black"</span><span>, </span><span>"brown"</span><span>]</span><span><br></span><span>for</span><span> i </span><span>in</span><span> range(len(colors)):</span><span><br></span><span>&nbsp; print(i+</span><span>1</span><span>, colors[i])</span><span><br></span><span># 1 white</span><span><br></span><span># 2 black</span><span><br></span><span># 3 brown</span></p></td></tr></tbody></table>

<p>Using the index on the initial list makes it more difficult to understand and encourages poor habits like changing original values. Particularly in complex nested data structures. </p>
<p>Additionally, we need to manually change the index's values into "counting numbers."</p>
<p>Code After Refactoring:</p>
<table><colgroup></colgroup><tbody><tr><td><p><span>colors = [</span><span>"white"</span><span>, </span><span>"black"</span><span>, </span><span>"brown"</span><span>]</span><span><br></span><span>for</span><span> i, colors </span><span>in</span><span> enumerate(colors, </span><span>1</span><span>):</span><span><br></span><span>&nbsp; print(i, colors)</span><span><br></span><span>&nbsp; </span><span><br></span><span># 1 white</span><span><br></span><span># 2 black</span><span><br></span><span># 3 brown</span><span><br><br></span></p></td></tr></tbody></table>

<p>The enumerate() method returns the current counter value and each item in the iterable and requires two arguments: the iterable and the initial count value.</p>
<p>Note: It is a typical error to order the temporary count variable first in the for loop and the initial count value second in the function call.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Hopefully, you now understand what code refactoring is along with the importance and reasons for refactoring your code.</p>
<p>Based on the simple Python code above, you should understand that refactoring does not necessarily mean your code needs to be shorter after refactoring – but just somehow simpler and more understandable.  </p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Make Calculation Tests Simpler and More Expressive with These New Refactorings ]]>
                </title>
                <description>
                    <![CDATA[ It is a good idea to make tests as descriptive as possible – to achieve Tests as Documentation. Including the calculations in the test is a big part of this, and avoids the Hard Coded Test Data smell. For example, if a test looks like this, it is har... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-test-complicated-calculations-new-refactoring/</link>
                <guid isPermaLink="false">66bb9264fce17a7d99885305</guid>
                
                    <category>
                        <![CDATA[ refactoring ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Testing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ unit testing ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Cedd Burge ]]>
                </dc:creator>
                <pubDate>Wed, 03 Feb 2021 17:32:34 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2021/02/unit-testing-calculations.JPG" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>It is a good idea to make tests as descriptive as possible – to achieve <a target="_blank" href="http://xunitpatterns.com/Goals%20of%20Test%20Automation.html#Tests%20as%20Documentation">Tests as Documentation</a>. Including the calculations in the test is a big part of this, and avoids the <a target="_blank" href="http://xunitpatterns.com/Obscure%20Test.html#Hard-Coded%20Test%20Data">Hard Coded Test Data</a> smell.</p>
<p>For example, if a test looks like this, it is hard to know what the problem is when it fails, as the test data is all hard coded. The test tells you very little about how the system under test (SUT) should behave, and so doesn’t act as documentation.</p>
<pre><code class="lang-python"><span class="hljs-keyword">assert</span> sut.wake_erosion_rate(<span class="hljs-number">0.03</span>) == <span class="hljs-number">0.8</span>
</code></pre>
<p>However, if you remove this code smell, and include the calculation in the test, it becomes obvious what the problem is when there is a failure, and the test does now act as documentation. </p>
<p>You might also want to replace the <code>2.5</code> and <code>0.05</code> with the names of the constants they represent.</p>
<pre><code class="lang-python">ambient_turbuluence = <span class="hljs-number">0.03</span>

<span class="hljs-keyword">assert</span> 
    sut.wake_erosion_rate(ambient_turbuluence) 
    == <span class="hljs-number">2.5</span> * ambient_turbuluence + <span class="hljs-number">0.05</span>
</code></pre>
<p>This article references the excellent <a target="_blank" href="https://www.goodreads.com/review/show/2179089513">XUnit Test Patterns book</a>, which defines the most widely accepted lexicon on unit test patterns and practices.</p>
<h2 id="heading-example-calculation">Example Calculation</h2>
<p>The rest of this article will discuss the <a target="_blank" href="https://github.com/ceddlyburge/unit-testing-calculations/blob/main/cash_flow_calculator/construction_margin_calculator.py#L31">example ConstructionMarginCalculator class</a>, and will describe refactorings that simplify the tests and allow them to feasibly include the calculations. </p>
<p>The calculation is around 30 lines long, and is worth a quick glance now if you have the time. But if not, don’t worry, the rest of this post will still make sense.</p>
<p>There are a few if statements and a loop, and in total these lead to 2^9, or 512, paths through the code! Eek! It is clearly not feasible (or useful) to test all these, hence the need to find ways to make it easier.</p>
<p>This example code has an <a target="_blank" href="https://github.com/ceddlyburge/unit-testing-calculations/blob/main/tests/test_construction_margin_calculator.py#L17">initial naive test</a>, which is around 30 lines long. It doesn’t include the calculations, and introducing them would make the test even longer and more complicated, so would be hard to justify.</p>
<p>Each of the following sections describes a refactoring, and includes links to the refactored example code. These build on the existing <a target="_blank" href="http://xunitpatterns.com/Test%20Refactorings.html">test refactorings</a> from <a target="_blank" href="https://www.goodreads.com/review/show/2179089513">XUnit Test Patterns</a>.</p>
<p>The refactorings reduce the number of paths through the code and simplify the test. This means that fewer tests are needed, and that the tests become small and simple enough to include the calculations.</p>
<h2 id="heading-extract-calculation-from-loop">Extract Calculation From Loop</h2>
<p>The code initially calculates values for a <em>list of</em> <code>steps</code>, which means that any test for it must take on this complication. </p>
<p>The setup of the test is harder, as we have to create a list as opposed to a single value. The assertions are harder as we have to assert on a list as opposed to a single value. Finally, we should have multiple tests (probably for 0, 1, and multiple items in the list).</p>
<p>The easy fix is simply to refactor the code so that it only calculates a single <code>step</code>. This displaces the iteration code to some other place, which might want testing. However, this can be tested using a Mock, so only the iteration needs to be tested (as opposed to the iteration <em>and</em> the calculation), which is simple, and potentially so simple that you don’t need to test it.</p>
<p>This refactoring means that instead of requiring 3 tests (for 0, 1, and multiple items in the list), there is now just one. This reduces the number of paths through the code from 2^9 to 2^7, or 128. This is already a lot better, but still too many to test!</p>
<ul>
<li><a target="_blank" href="https://github.com/ceddlyburge/unit-testing-calculations/blob/main/cash_flow_calculator/construction_margin_calculator_without_loop.py#L31">Refactored Calculation</a></li>
<li><a target="_blank" href="https://github.com/ceddlyburge/unit-testing-calculations/blob/main/tests/test_construction_margin_calculator_remove_loop.py#L11">Refactored Test</a></li>
</ul>
<h2 id="heading-introduce-mockable-abstractions">Introduce Mockable Abstractions</h2>
<p>The details of the inflation calculation aren’t shown in the example, but they are reasonably complicated.</p>
<p>To avoid this we can change the code to accept an <code>InflationCalculator</code>. The inflation calculation always uses the same <code>date_of_financial_close</code>, <code>inflation_rate</code> and <code>inflation_mode</code>, which means that it can take these in the constructor. This in turn means that the main calculation no longer requires the <code>inflation_rate</code> and <code>inflation_mode</code>.</p>
<p>Then in the tests we can create a mock <code>InflationCalculator</code>. This could for example always return a value of <code>2</code>, which makes the overall calculation a lot simpler.</p>
<p>It is also easy to imagine that inflation calculations will happen in other parts of the code, so the abstraction will be widely useful.</p>
<p>This step means that instead of 4 conditional branches in the inflation calculation, there is now just one. This reduces the number of paths through the code from 2^7 to 2^3, or just 8. It is also necessary to test the <code>InflationCalculator</code>, and this has 4 branches, so needs 4 tests, but this still only makes 12 tests in total. Yay for loosely coupled code!</p>
<p>We now have a feasible number of tests to write, but including the calculation in the test is still going to be very cumbersome. Luckily we still have some refactorings left up our sleeve.</p>
<ul>
<li><a target="_blank" href="https://github.com/ceddlyburge/unit-testing-calculations/blob/main/cash_flow_calculator/construction_margin_calculator_mockable_abstraction.py#L29">Refactored Calculation</a></li>
<li><a target="_blank" href="https://github.com/ceddlyburge/unit-testing-calculations/blob/main/tests/test_construction_margin_calculator_mockable_abstraction.py#L11">Refactored Test</a></li>
</ul>
<h2 id="heading-test-conditional-branches-in-isolation">Test Conditional Branches in Isolation</h2>
<p>The code branches based on certain conditionals. We can simply make some of these conditionals <code>False</code>, and then test each branch of the code in isolation. This way, each test only has to include the calculation for the branch that it is concerned with.</p>
<p>For example, we can set <code>in_selling_mode</code> to be <code>False</code> and <code>step.start_of_step</code> to be different to <code>date_of_financial_close</code>. This makes the test simple enough that it is feasible for it to perform the same calculation that the code does. </p>
<p>This in turn means that the test clearly communicates that the <code>turbine_cost_including_margin</code> should be the <code>turbine_costs * fraction_of_spend * inflation</code>. This helps readers understand the calculator, and achieves the goal of <a target="_blank" href="http://xunitpatterns.com/Goals%20of%20Test%20Automation.html#Tests%20as%20Documentation">Tests as Documentation</a></p>
<p>At the moment the test is still quite long. However, because we are only testing a small part of the calculation, a lot of this information is now irrelevant (the <code>any_double</code> variable). This means we can now create a <a target="_blank" href="http://natpryce.com/articles/000714.html">Test Data Builder</a>, or use helper functions to make things more concise. We will see an example of this in the next refactoring.</p>
<ul>
<li><a target="_blank" href="https://github.com/ceddlyburge/unit-testing-calculations/blob/main/cash_flow_calculator/construction_margin_calculator_mockable_abstraction.py#L29">No Change to Calculation</a></li>
<li><a target="_blank" href="https://github.com/ceddlyburge/unit-testing-calculations/blob/main/tests/test_construction_margin_calculator_isolate_branches.py#L16">Refactored Test</a></li>
</ul>
<h2 id="heading-test-values-in-isolation">Test Values in Isolation</h2>
<p>“Test Conditional Branches in Isolation” is a useful technique, but it can still leave us with some complications if a value is calculated / updated in multiple branches. </p>
<p>A good example of this is <code>balance_of_plant_cost_including_margin</code>, which is set initially, and then updated in the <code>if (in_selling_mode)</code> branch.</p>
<p>Testing <code>balance_of_plant_cost_including_margin</code> in isolation allows the test to concentrate just on this one value / calculation, which means that a lot less setup is required. The <a target="_blank" href="http://natpryce.com/articles/000714.html">Test Data Builder</a> pattern hides <a target="_blank" href="http://xunitpatterns.com/Obscure%20Test.html#Irrelevant%20Information">Irrelevant Information</a>, and the test becomes more concise and expressive.</p>
<p>Including the calculation continues to make the test clearly communicate its intent and act as documentation. Interestingly the test calculation code is no longer an exact copy of the SUT calculation code, as it already knows that it is <code>in_selling_mode</code>, so doesn’t need a conditional statement. This means that the test is simpler than the code, which helps avoid the <a target="_blank" href="http://xunitpatterns.com/Obscure%20Test.html">Obscure Test</a> smell.</p>
<ul>
<li><a target="_blank" href="https://github.com/ceddlyburge/unit-testing-calculations/blob/main/cash_flow_calculator/construction_margin_calculator_mockable_abstraction.py#L29">No Change to Calculation</a></li>
<li><a target="_blank" href="https://github.com/ceddlyburge/unit-testing-calculations/blob/main/tests/test_construction_margin_calculator_isolate_values.py#L18">Refactored Test</a></li>
</ul>
<h2 id="heading-test-partial-values-in-isolation">Test Partial Values in Isolation</h2>
<p>Sometimes the calculation of individual values is very complex, and can’t be split up by conditional branches. This can make it challenging to include the calculation in the test. <code>construction_profit</code> is a reasonable example of this, which is calculated as follows:</p>
<pre><code class="lang-python">step.turbine_cost_including_margin = 
 turbine_costs * inflation * fraction_of_spend;

step.balance_of_plant_cost_including_margin = 
 balance_of_plant_costs_at_financial_close * inflation * fraction_of_spend;

step.construction_profit = 
 <span class="hljs-number">-1</span> * 
 (step.turbine_cost_including_margin + step.balance_of_plant_cost_including_margin) *
 epc_margin
</code></pre>
<p><code>inflation</code>, <code>fraction_of_spend</code> and <code>epc_margin</code> have a multiplicative effect, so if we set them to <code>1</code>, they won’t have any effect and we can easily write a test for the rest of the logic.</p>
<p><code>step.turbine_cost_including_margin</code> and <code>step.balance_of_plant_cost_including_margin</code> have an additive effect, so if we set them to <code>0</code>, again they won’t have any effect and we can easily write a test for the rest of the logic.</p>
<p>Testing just a portion of <code>construction_profit</code> in isolation allows the test to concentrate just on this part of the calculation, which, as before, makes the test shorter and simpler, and avoids the <a target="_blank" href="https://hackmd.io/4xuiUbrAQimsWCknJiqXNw?both">Obscure Test</a> smell.</p>
<ul>
<li><a target="_blank" href="https://github.com/ceddlyburge/unit-testing-calculations/blob/main/cash_flow_calculator/construction_margin_calculator_mockable_abstraction.py#L29">No Change to Calculation</a></li>
<li><a target="_blank" href="https://github.com/ceddlyburge/unit-testing-calculations/blob/main/tests/test_construction_margin_calculator_isolate_partial_values.py#L14">Refactored Test</a></li>
</ul>
<h2 id="heading-introduce-the-blackboard-pattern">Introduce the Blackboard Pattern</h2>
<p>The Blackboard pattern is a more involved technique, and is often badly understood. But essentially it involves using a “blackboard” to break the dependencies within complicated calculations.</p>
<p>In this example the <code>construction_profit</code> depends on <code>turbine_cost_including_margin</code> and <code>balance_of_plant_cost_including_margin</code>, which are themselves calculated from the inputs. This makes testing harder. </p>
<p>In order to test the <code>construction_profit</code> you essentially also have to test <code>turbine_cost_including_margin</code> and <code>balance_of_plant_cost_including_margin</code>.</p>
<p>When we introduce the blackboard pattern, one calculator writes the <code>turbine_cost_including_margin</code> and <code>balance_of_plant_cost_including_margin</code> to the blackboard, and when calculating the <code>construction_profit</code> we read these values from the blackboard.</p>
<p>This breaks the connection between the two things, so when testing we can just add values for <code>turbine_cost_including_margin</code> and <code>balance_of_plant_cost_including_margin</code> to the blackboard.</p>
<ul>
<li><a target="_blank" href="https://github.com/ceddlyburge/unit-testing-calculations/blob/main/cash_flow_calculator/construction_margin_calculator_blackboard_pattern.py#L15">Refactored Calculation</a></li>
<li><a target="_blank" href="https://github.com/ceddlyburge/unit-testing-calculations/blob/main/tests/test_construction_margin_calculator_blackboard_pattern.py#L12">Refactored Test</a></li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>When testing calculations, it is important to include the calculation in the tests. This avoids the <a target="_blank" href="http://xunitpatterns.com/Obscure%20Test.html#Hard-Coded%20Test%20Data">Hard Coded Test Data</a> smell, allows the tests to clearly express their intent and achieve <a target="_blank" href="http://xunitpatterns.com/Goals%20of%20Test%20Automation.html#Tests%20as%20Documentation">Tests as Documentation</a>.</p>
<p>The refactorings described in this article allow you to do this while also making sure that the tests are concise and understandable.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Why You Should Refactor Your Code ]]>
                </title>
                <description>
                    <![CDATA[ Raise your hand if any of the following sounds familiar: Think about code formatting. Get rid of unnecessary <div>'s and <span>'s. Use functional React components. Try to avoid arrow function in render. And do not repeat yourself! Before I go straigh... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/why-you-should-refactor-your-code/</link>
                <guid isPermaLink="false">66be14d94acec4a9c337d05e</guid>
                
                    <category>
                        <![CDATA[ clean code ]]>
                    </category>
                
                    <category>
                        <![CDATA[ refactoring ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Oleh Romanyuk ]]>
                </dc:creator>
                <pubDate>Fri, 03 Apr 2020 20:00:25 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2020/04/refactor-your-react-code.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Raise your hand if any of the following sounds familiar: Think about code formatting. Get rid of unnecessary <code>&lt;div&gt;</code>'s and <code>&lt;span&gt;</code>'s. Use functional React components. Try to avoid arrow function in render. And do not repeat yourself!</p>
<p>Before I go straight to refactoring, I need you to answer one simple question: What does it mean to develop an application? Usually, this means producing a piece of software that meets requirements by implementing certain features. </p>
<p>And how do we do that? We collect customer requirements, estimate them, and develop features one by one, right? Almost.</p>
<h2 id="heading-do-not-forget-about-bugs"><strong>Do Not Forget About Bugs</strong></h2>
<p>Yes, errors do occur. Depending on the development process, software complexity, technical stack, and many other parameters, the number of bugs may vary. </p>
<p>A business cannot afford critical issues in production. To minimize problems, you should pay particular attention to the <a target="_blank" href="https://keenethics.com/blog/qa-documentation">QA process</a>. But QA theory claims that it is usually impossible to run 100% test coverage of your apps and prepare for all possible scenarios. </p>
<p>Still, to achieve optimal results, teams spend a lot of time testing software and fixing issues. This is a necessary part of the process that each customer should understand and prioritize.</p>
<h2 id="heading-mind-the-technical-debt"><strong>Mind the Technical Debt</strong></h2>
<p>Yet, this coin has a flip side. The longer the development and testing process takes, the more technical debt you incur. </p>
<p>So, what does "technical debt" mean? Technical debt refers to all the quality-related issues you have in your code. Issues which will require spending additional resources in the future. </p>
<p>You incur technical debt for a variety of reasons, such as:</p>
<ol>
<li>The business pushes to release new features faster.</li>
<li>Testing is insufficient.</li>
<li>The requirements are changing rapidly.</li>
<li>The developers are inexperienced.</li>
</ol>
<p>Technical debt should be documented. If you do not leave to-do's in the code, you will most likely forget about the issue. And even if you have time for it in the future, you will not remember to fix it.</p>
<h2 id="heading-understand-the-importance-of-refactoring"><strong>Understand the Importance of Refactoring</strong></h2>
<p>Usually, you need to spend some time refactoring the existing code in order to solve code quality issues and thus lower technical debt. </p>
<p>But what is refactoring? It is the process of restructuring the existing code without changing its external behavior. And that is actually something that might be difficult to understand for business people managing the project.</p>
<blockquote>
<p>– Will we get any new features?   </p>
<p>– No.   </p>
<p>– Will we at least fix some bugs?   </p>
<p>– Also no.    </p>
<p>– What will we get then?   </p>
<p>– …</p>
</blockquote>
<p>Working with technical debt helps to avoid bugs. And to add fixes or changes to the project, we always need to read the old code. </p>
<p>Therefore, refactoring and maintaining good code quality will help us keep development at a good pace. </p>
<p>Sometimes a business might not need it. For instance, if you're working on a prototype or <a target="_blank" href="https://keenethics.com/services-proof">Proof of Concept</a>, or if there are business priorities that cannot be adjusted, you can do without refactoring. </p>
<p>But in most cases, cutting out refactoring is not a wise thing to do. You might spend a huge amount of time on refactoring if your developers are perfectionists, but this makes no sense either. </p>
<p>Therefore, you need to strike a balance. You should not spend more time refactoring than you will save in the future.</p>
<h2 id="heading-how-to-start-refactoring-your-react-code"><strong>How to Start Refactoring Your React Code</strong></h2>
<h3 id="heading-think-about-code-formatting"><strong>Think about code formatting</strong></h3>
<p>Some people add trailing commas, and some don't. Some use single quotes, while others use double quotes for a string. </p>
<p>If you work in a team, maintaining the common code style can be really difficult. And inconsistency in code style can make your code look dirty and hard to read. </p>
<p>So if you haven’t thought about using code formatting tools before, it’s high time to do so. One of the most popular and easy-to-use React refactoring tools is <a target="_blank" href="https://prettier.io/">Prettier</a>. You can just add it to the project and it will take care of formatting for you. </p>
<p>Prettier has some default style settings, but you can change them according to your preferences by adding a <code>.prettierrc</code> file with your formatting rules. </p>
<p>A good setup of <code>.prettierrc</code> may look like this:</p>
<pre><code class="lang-js">{ <span class="hljs-string">"printWidth"</span>: <span class="hljs-number">120</span>,  <span class="hljs-string">"singleQuote"</span>: <span class="hljs-literal">true</span>, “trailingComma”: “none” }
</code></pre>
<p>You can also automatically reformat the code before committing with <a target="_blank" href="https://prettier.io/docs/en/precommit.html">pre-commit hooks</a>.</p>
<h3 id="heading-get-rid-of-unnecessary-s-and-s"><strong>Get rid of unnecessary <div>’s and <span>’s</span></div></strong></h3>
<p>When React 16.2 was released in November 2017, a lot of React developers sighed in relief. Prior to that, for a component to return a list of children, it was necessary to wrap the children in an extra element, such as <code>&lt;div&gt;</code> or <code>&lt;span&gt;</code>. </p>
<p>But with React 16.2 we received improved support for returning components’ children. Now developers can use so-called fragments. They look like empty JSX tags (<code>&lt;&gt; … &lt;/&gt;</code>). With the help of fragments, you can pass a list of children to the component without adding extra nodes to the DOM.</p>
<h3 id="heading-think-about-names"><strong>Think about names</strong></h3>
<p>Don’t be lazy when you're thinking about names for components and variables. Every name should be self-explanatory.</p>
<p>Have you ever seen code snippets like this?</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> modifyData = data.map(<span class="hljs-function"><span class="hljs-params">x</span> =&gt;</span> [x.a, x.b]))
</code></pre>
<p>What does it do? If you cannot understand the purpose of a variable from its name, it is time to rename it! </p>
<p>This will help you and your team understand the logic more easily. It will also eliminate the time spent making changes to existing components in the future.</p>
<h3 id="heading-dont-repeat-yourself"><strong>Don’t Repeat Yourself</strong></h3>
<p>The DRY principle was first formulated in the book <em>The Pragmatic Programmer</em>. It states that "every piece of knowledge must have a single, unambiguous, authoritative representation within a system". In other words, you need to put repetitive code blocks into separate reusable components. </p>
<p>Making your code DRY has a lot of benefits. It can save you a lot of time. If you need to change this code in the future, you will only do that in one place. Also, you will never have to worry that you forgot to make changes in some places. Furthermore, you will keep the components cleaner and increase the readability of code. </p>
<p>To keep your components DRY and small, you can follow two simple rules:</p>
<ol>
<li>If you use a code block more than two times, it’s time to extract it.</li>
<li>If you exceed a predefined number of lines in a component (e.g. 100 lines), there is probably logic that can be extracted. Divide it into smaller components by functionality.</li>
</ol>
<h3 id="heading-use-functional-over-class-components"><strong>Use functional over class components</strong></h3>
<p>With the introduction of Hooks in React 16.8, we received access to React class features in functional components. Hooks solve a bunch of problems frequently encountered by developers over the past few years. </p>
<p>For example, the <code>useEffect</code> hook, as the React docs suggest, allows us to group the component logic into small functions based on what pieces are related (instead of grouping the logic based on life-cycle methods). This helps us to better restructure our logic. </p>
<p>All in all, refactoring React components with the help of hooks makes the code cleaner and reduces the amount of code you need to write. </p>
<p>Here is a very basic example: fetching the data after the component has mounted and re-fetching it based on the updated props. </p>
<p>In a class component, we would write something like this:</p>
<pre><code class="lang-js"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BookList</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">React</span>.<span class="hljs-title">Component</span> </span>{
  componentDidMount() {
    <span class="hljs-built_in">this</span>.props.fetchBooks(<span class="hljs-built_in">this</span>.props.bookGenre);
  }
  componentDidUpdate(prevProps) {
    <span class="hljs-keyword">if</span> (prevProps.bookGenre !== <span class="hljs-built_in">this</span>.props.booksGenre) {
      <span class="hljs-built_in">this</span>.props.fetchBooks(<span class="hljs-built_in">this</span>.props.bookGenre);
    }
  } 
<span class="hljs-comment">// ... }</span>
</code></pre>
<p>With React hooks it will look like this:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> BookList = <span class="hljs-function">(<span class="hljs-params">{ bookGenre, fetchBooks }</span>) =&gt;</span> {
  useEffect(<span class="hljs-function">() =&gt;</span> {
    fetchBooks(bookGenre);
  }, [bookGenre]);
<span class="hljs-comment">// ... }</span>
</code></pre>
<p>The books fetching logic is now gathered in one place. The <code>useEffect</code> hook will run after mount each time the props <code>[bookGenre]</code> in square brackets change. Much cleaner, isn’t it? </p>
<p>Also, you can extract similar stateful logic and reuse it in different components by creating your custom hooks. You can read more about custom hooks in the official <a target="_blank" href="https://reactjs.org/docs/hooks-custom.html">React documentation</a>.</p>
<h3 id="heading-try-to-avoid-arrow-functions-in-render"><strong>Try to avoid arrow functions in render</strong></h3>
<p>Have you ever seen code like this?:</p>
<pre><code class="lang-js">render() {    
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span> this.setState({ flag: true })} /&gt;
      ...      
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>    
  );  
}
</code></pre>
<p>Sure you have. What is the problem with it? Every time a component is rendered, a new instance of such a function is created. </p>
<p>It is not a big deal if the component is rendered one or two times. But in other cases, it can really affect performance. So, if you care about performance, declare the function before using it in render:</p>
<pre><code class="lang-js">changeFlag = <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">this</span>.setState({ <span class="hljs-attr">flag</span>: <span class="hljs-literal">true</span> })
render() {    
  <span class="hljs-keyword">return</span> (      
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span> 
      <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{this.changeFlag}</span> /&gt;</span>        
      ...      
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>    
  );  
}
</code></pre>
<h3 id="heading-make-the-bundle-smaller"><strong>Make the bundle smaller</strong></h3>
<p>If you are using a third-party library, you should not load the entire thing if it isn't necessary. Sometimes you can stumble upon an import which uses only one method from the library, such as:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> lodash form <span class="hljs-string">'lodash'</span>  
...  
const certainProps = lodash.pick(userObject, [<span class="hljs-string">'name'</span>, <span class="hljs-string">'email'</span>]);  ...
</code></pre>
<p>Instead, it is better to use the following:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> pick <span class="hljs-keyword">from</span> <span class="hljs-string">'lodash/pick'</span> 
... 
const certainProps = pick(userObject, [<span class="hljs-string">'name'</span>, <span class="hljs-string">'email'</span>]); ...
</code></pre>
<p>Now you do not load the whole library, just the method you need.</p>
<h2 id="heading-to-wrap-up"><strong>To Wrap Up</strong></h2>
<p>Let's review the steps you should take to refactor your React code:</p>
<ul>
<li>Think about code formatting</li>
<li>Get rid of unnecessary <code>&lt;div&gt;</code>'s and <code>&lt;span&gt;</code>'s</li>
<li>Think about names</li>
<li>Don’t Repeat Yourself</li>
<li>Use functional over class components</li>
<li>Try to avoid arrow functions in render</li>
<li>Make the bundle smaller</li>
</ul>
<p>Yet, ideal refactoring is refactoring that does not occur. As a developer and especially as a tech lead, you should think many steps ahead and try to produce high-quality code. You should also carry out regular code reviews, not only within one team but also between teams.</p>
<h2 id="heading-do-you-have-an-idea-for-a-react-project">Do you have an idea for a React project?</h2>
<p>My company KeenEthics is an experienced React <a target="_blank" href="https://keenethics.com/services-web-development">development company</a>. If you are ready to change the game with your software project, feel free to <a target="_blank" href="https://keenethics.com/services-web-development">request an estimate</a>.</p>
<p>You can read more similar articles on my Keen Blog. Allow me to suggest <a target="_blank" href="https://keenethics.com/blog/the-value-of-user-testing">The Value of User Testing</a> or <a target="_blank" href="https://keenethics.com/blog/angular-vs-react-what-to-choose-for-your-app">Angular vs React: What to Choose for Your App?</a></p>
<p>Most importantly, I would like to say "thank you" to Yaryna Korduba and Max Fedas, both outstanding React developers, for coauthoring this article as well as the readers for making it to the end!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How Code Becomes A Dumping Ground (And How To Fix It!) ]]>
                </title>
                <description>
                    <![CDATA[ By James Hickey Earlier in my career, I faced a sort of career crisis. I was part of a team creating a large analytics platform in the automotive industry. This application had the typical "enterprise-y" layered architecture you would expect ("Busine... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/fixing-code-dumping-grounds/</link>
                <guid isPermaLink="false">66d45f31a3a4f04fb2dd2e57</guid>
                
                    <category>
                        <![CDATA[ clean code ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Code Quality ]]>
                    </category>
                
                    <category>
                        <![CDATA[ refactoring ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Wed, 18 Sep 2019 20:36:52 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2019/09/ashim-d-silva-Kw_zQBAChws-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By James Hickey</p>
<p>Earlier in my career, I faced a sort of career crisis.</p>
<p>I was part of a team creating a large analytics platform in the automotive industry. This application had the typical "enterprise-y" layered architecture you would expect ("Business Layer", "Data Access Layer", "Core", etc.).</p>
<p>You would expect to find business logic - the really important business logic - embedded somewhere inside of the code from these layers. But, usually, the really important business rules were coded into stored procedures.</p>
<p><strong>Stored procedures</strong>, if you don't know, is like a function you create inside of a database that uses an SQL-like syntax to process data, store it, etc.</p>
<p>I wondered what the purpose of the layers was. They didn't have any code except for passing data to stored procedures or showing data returned by one.</p>
<p>I started to learn more about object-oriented programming, industry best-practices, SOLID, other programming paradigms, application architecture, etc.</p>
<p>From this career crisis, I discovered that these problems have already been solved! It just takes research, time and practice to learn and grow skilled in them.</p>
<h2 id="heading-object-oriented">Object-Oriented?</h2>
<p>One thing I discovered is that all the projects I've been on that were doing "Object-Oriented" programming were <em>not</em> doing true OOP. Just because you use classes doesn't mean you are doing OOP. Especially if you are using stored procedures to encode all your business rules.</p>
<h3 id="heading-a-short-aside-the-great-debate">A Short Aside: The Great Debate</h3>
<p>It needs to be brought up: is object-oriented programming or functional programming better?</p>
<p>For starters, most people don't understand what OOP was intended to be in the first place. Similar to how Agile today is usually misunderstood (e.g. just because you are having daily stand-ups, using story points, kanban, etc. it doesn't mean you are doing Agile).</p>
<p>Alan Kay is considered one of the fathers of OOP. <a target="_blank" href="http://userpage.fu-berlin.de/~ram/pub/pub_jf47ht81Ht/doc_kay_oop_en">In a certain email</a>, he gave some frank explanations about what OOP was supposed to be.</p>
<blockquote>
<p>"I thought of objects being like biological cells and/or individual computers on a network, only able to communicate with messages (so messaging came at the very beginning -- it took a while to see how to do messaging in a programming language efficiently enough to be useful)...</p>
<p>OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things...</p>
<p>But just to show how stubbornly an idea can hang on, all through the seventies and eighties, there were many people who tried to get by with 'Remote Procedure Call' instead of thinking about objects and messages."</p>
</blockquote>
<p>For those familiar with microservices, the actor model, and other advanced programming paradigms, your Spidey sense is tingling. These are actually more closely related to true OOP.</p>
<p>So, is FP better than true OOP?</p>
<p>I don't think so. I think they both have their merits. Many languages today are embracing both paradigms and allowing developers to use the tools and methods that work best for the given problem!</p>
<p>What you'll usually find are classes that expose all their properties or internal data members. An HTTP request or database query will fill-up all the properties, and then, perhaps, something else will work on that object's data.</p>
<p>Instead of what Alan Kay intended to be as little "bundles" that pass messages to each other (see aside above), most developers are using objects as mere "data holders". Glorified variables, as it were.</p>
<p>What you'll also find in many codebases are very generic classes like <code>User</code>, <code>Customer</code> or <code>Order</code>.</p>
<p>Is that bad? Well, <strong>yes</strong>.</p>
<p>Let me ask you a question:</p>
<p>Is <code>User</code> used in many different unrelated places in your application?</p>
<p>For example, is your <code>User</code> class used in the billing part of your code, the user profile parts, the shipping parts, etc.?</p>
<p>Most systems are doing something like that.</p>
<p>What will end up happening is that, because these classes are so generic, <strong>they become dumping grounds for code which we don't know where it belongs.</strong></p>
<p>Instead of taking the time to think about the business need for this new code we've written, often we feel that it's easier to put into our generic classes. It's all shareable, right? And we're all about code-reuse, right?</p>
<h2 id="heading-coupling">Coupling</h2>
<p>So... what if I changed the <code>User</code> class to conform to some billing logic? What are the chances that I've also broken the shipping feature by changing this class? I don't know, but it's <strong>higher than 0%.</strong></p>
<p>This <code>User</code> class has coupled all your features together. This causes lots of problems.</p>
<p>Ideally, we want our code to be orthogonal (that's just a fancy word that means changing code in one place won't affect other unrelated places).</p>
<p>We want to be able to change the shipping feature, for example, and <strong>not have to test our entire application again.</strong> But, if we're sharing our <code>User</code> class everywhere then, to have confidence that we didn't break stuff, we need to re-test <strong>everything.</strong></p>
<p>This leads to a fear of changing code. The fear of making our code better. It also leads to a lot of bugs.</p>
<p>If you are building out the payment feature for your application - you shouldn't have to think about whether you are breaking the shipping feature at the same time! This causes a huge cognitive load that doesn't need to be there.</p>
<h3 id="heading-another-aside-warning-sign">Another Aside: Warning Sign</h3>
<p>Overall, I find that the idea of segmenting your business features/functions via different physical folders or even entirely different projects altogether is best. <a target="_blank" href="https://dev.to/jamesmh/the-life-changing-and-time-saving-magic-of-feature-focused-code-organization-1708">I've written about this before</a>.</p>
<p>But, when it comes to our code at a deeper level, we can still tend to design our classes and objects in a way that's still too generic and leads to much coupling.</p>
<p>Anytime I find classes that have simple names like <code>User</code> or <code>Customer</code>, a warning signal goes off. I'd much rather see classes that are created for a specific context.</p>
<p>For example, if I saw a class named <code>UserForAuthentication</code> or <code>PaymentsCustomer</code> then I would be more confident that these classes aren't being thrown around and reused in too many contexts.</p>
<p>Here's a basic way that might help get started on analyzing your classes. Take your class name and answer these questions:</p>
<p>Is there a subject? (user, client, order, etc.)</p>
<p>Is there a context for that subject? (shipping, orders, dashboard, etc.)</p>
<p>Is there even perhaps an action being performed on the subject? (as we'll see in more details soon)</p>
<p>If you cannot answer 2 of those questions then I'd say there's a good chance that your class might be doing too much by being too generic.</p>
<h2 id="heading-one-of-these-things-is-not-like-the-others">One Of These Things Is Not Like The Others</h2>
<p>There's a programming principle called the Single Responsibility Principle.</p>
<p>When looking at classes or methods that are doing too much, using the SRP as a guiding light can help us to make code that is easier to maintain, less coupled and therefore leads to fewer bugs.</p>
<p>Let's look at a generic <code>User</code> class that might be similar to code you've seen before:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">class</span> User {
    <span class="hljs-keyword">public</span> firstName: <span class="hljs-built_in">string</span>;
    <span class="hljs-keyword">public</span> lastName: <span class="hljs-built_in">string</span>;
    <span class="hljs-keyword">public</span> id: <span class="hljs-built_in">number</span>;
    <span class="hljs-keyword">public</span> jwtToken: <span class="hljs-built_in">string</span>;
    <span class="hljs-keyword">public</span> homeAddress: <span class="hljs-built_in">string</span>;
    <span class="hljs-keyword">public</span> creditCardNo: <span class="hljs-built_in">string</span>;

    <span class="hljs-keyword">public</span> getFullName(): <span class="hljs-built_in">string</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.firstName + <span class="hljs-string">" "</span> + <span class="hljs-built_in">this</span>.lastName;
    }

    <span class="hljs-keyword">public</span> decodeJwtToken(): <span class="hljs-built_in">string</span> {
        <span class="hljs-keyword">return</span> decode(<span class="hljs-built_in">this</span>.jwtToken);
    }
}
</code></pre>
<p>Look familiar?</p>
<p>Given the name of the class, we should start to be suspicious that it's too generic of a class.</p>
<h2 id="heading-you-have-mail">You Have Mail</h2>
<p>You've been tasked with adding a new business requirement. We need users to be able to pay for their products using PayPal.</p>
<p>This <code>User</code> class is already used in multiple places like the user profile, shipping and payment features.</p>
<p>All we need to do is add the user's PayPal email address to the user. Right?</p>
<h2 id="heading-breaking-it-up">Breaking It Up</h2>
<p>Usually, you will get new business requirements that require <em>more</em> changes to your code than this. But this is a simple example.</p>
<p>If we start changing this <code>User</code> class so that it works with the payment feature then we risk affecting the user profile or the shipping feature (since they use this class too).</p>
<p>What should we do?</p>
<p>The best thing to do here is <em>create a different user class that's used within each specific context.</em></p>
<p>Out of this should come classes like <code>UserForAuthentication</code>, <code>UserProfileUser</code>, <code>ShippingUser</code> and <code>PaymentUser</code>.</p>
<p>Are those models/classes going to contain similar pieces of data that all of them will need? Sure.</p>
<p>Will they also have pieces of data that are only used in one context? Sure.</p>
<p>For example, the user's <code>id</code> is needed everywhere.</p>
<p>But, the user's home address is only ever needed by shipping. Why then, does the payment feature need access to that data? It doesn't.</p>
<p>Here's what these classes might look like:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">class</span> UserProfileUser {
    <span class="hljs-keyword">public</span> firstName: <span class="hljs-built_in">string</span>;
    <span class="hljs-keyword">public</span> lastName: <span class="hljs-built_in">string</span>;
    <span class="hljs-keyword">public</span> id: <span class="hljs-built_in">number</span>;
    <span class="hljs-keyword">public</span> homeAddress: <span class="hljs-built_in">string</span>;

    <span class="hljs-keyword">public</span> getFullName(): <span class="hljs-built_in">string</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.firstName + <span class="hljs-string">" "</span> + <span class="hljs-built_in">this</span>.lastName;
    }
}

<span class="hljs-keyword">class</span> ShippingUser {
    <span class="hljs-keyword">public</span> id: <span class="hljs-built_in">number</span>;
    <span class="hljs-keyword">public</span> homeAddress: <span class="hljs-built_in">string</span>;
}

<span class="hljs-keyword">class</span> UserForAuthentication {
    <span class="hljs-keyword">public</span> id: <span class="hljs-built_in">number</span>;
    <span class="hljs-keyword">public</span> jwtToken: <span class="hljs-built_in">string</span>;

    <span class="hljs-keyword">public</span> decodeJwtToken(): <span class="hljs-built_in">string</span> {
        <span class="hljs-keyword">return</span> decode(<span class="hljs-built_in">this</span>.jwtToken);
    }
}

<span class="hljs-keyword">class</span> PaymentUser {
    <span class="hljs-keyword">public</span> id: <span class="hljs-built_in">number</span>;
    <span class="hljs-keyword">public</span> creditCardNo: <span class="hljs-built_in">string</span>;
}
</code></pre>
<h2 id="heading-keep-separate-things-separate">Keep Separate Things Separate</h2>
<p>Notice that the home address is needed by <code>UserProfileUser</code> and <code>ShippingUser</code>. Is that bad?</p>
<p>We've had it drilled into us so much that duplicating code is a bad thing. So much so, that it's this idea that's caused the problems we're talking about right now!</p>
<p>Sometimes, it's better to "duplicate" code and/or data - if they are within different contexts. Again, we want to avoid coupling our features and classes together.</p>
<p>Let me ask you a question:</p>
<p><em>Is it probable that the behavior for the home address within the user profile will be different than the behavior for it in the shipping feature?</em></p>
<p>The answer: <strong>yes.</strong></p>
<p>So then, we are talking about two different things. It's the same raw data but <strong>not the same business function or concept</strong><em>.</em></p>
<p>Shipping needs the home address so that it knows <strong>where to send products.</strong></p>
<p>The user profile needs the home address <strong>so the user can update its values from a UI.</strong></p>
<p><em>Not the same things.</em></p>
<p>Also, consider that it might also make sense to add an address to the <code>PaymentUser</code> class too. But, should this context share the same address as shipping?</p>
<p>Well, is it possible that your shipping address wouldn't be the same address you want to bill to? Sure! This happens all the time!</p>
<p>Using the Single Responsibility Principle, we see that these two concepts/responsibilities should be kept separate.</p>
<p>Also, notice that <em>most</em> of our pieces of data are <strong>not</strong> being shared. The JWT token, for example, is only needed for authenticating a user. Why would we ever need that piece of data inside our Shipping feature's code?</p>
<p>Now, that information is isolated.</p>
<p><strong>Also, any methods that act on that data will also be moved and not inappropriately called by another feature's code.</strong></p>
<p>This was a simple example, and in most cases, this can get a little trickier than we might want. In the end, though, keeping different business concepts separate from each other will make your code easier to understand within a specific context, easier to maintain and will become less error-prone!</p>
<p>This is an excerpt from my book  <a target="_blank" href="https://leanpub.com/refactoringtypescript">"Refactoring TypeScript: Keeping Your Code Healthy"</a>. This section of the book has <strong>more</strong> techniques for you to help deal with these kinds of dumping grounds. If you enjoyed this article then <a target="_blank" href="https://leanpub.com/refactoringtypescript">check out the book for more content like this!</a></p>
<p>You can <a target="_blank" href="https://twitter.com/jamesmh_dev">connect with me</a> on Twitter too.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ When to refactor legacy code vs. when to stay focused on your current project ]]>
                </title>
                <description>
                    <![CDATA[ By Sihui Huang When I work on projects, I often run into legacy code that can be improved — to be more readable, more testable, or more compliant with the current coding style. My urge to refactor the code is especially strong after spending a good a... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/when-to-refactor-legacy-code-vs-when-to-stay-focused-on-your-current-project-fda99919cc34/</link>
                <guid isPermaLink="false">66c3665109a9333511bcdb63</guid>
                
                    <category>
                        <![CDATA[ Developer ]]>
                    </category>
                
                    <category>
                        <![CDATA[ General Programming ]]>
                    </category>
                
                    <category>
                        <![CDATA[ refactoring ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Software Engineering ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Fri, 08 Feb 2019 21:14:01 +0000</pubDate>
                <media:content url="https://cdn-media-1.freecodecamp.org/images/1*qw5zQRKKw4pNbhv3nwjIjQ.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Sihui Huang</p>
<p>When I work on projects, I often run into legacy code that can be improved — to be more readable, more testable, or more compliant with the current coding style. My urge to refactor the code is especially strong after spending a good amount of time trying to understand a piece of obscure code. <a target="_blank" href="https://www.sihui.io/brain-friendly-code/">That code hurts my brain</a>, and I don’t want the same thing to happen to other developers. There is (the adaptation of) the famous Boy Scout Rule: <strong>Leave Code Better than You Found It</strong>.</p>
<p>But at the same time, I also want to stay focused and make progress on my current projects. I worry that I might get distracted and spend too much time refactoring.</p>
<p>This is a dilemma I often face. <strong>I want to be the good samaritan leaving code better than I found it, an engineer turning all the messy code I come across into something clean and elegant. But I also want to stay pragmatic and ship projects quickly.</strong></p>
<p>Balancing between refactoring the legacy code I come across and staying focused on my current project can be hard.</p>
<p>A recent conversation with my manager on this topic provided me guidance to find that balance. Here are a few points I find helpful.</p>
<h4 id="heading-you-dont-have-to-make-the-code-perfect-small-improvements-are-still-useful"><strong>You don’t have to make the code perfect. Small improvements are still useful.</strong></h4>
<p>After I read a piece of legacy code, my brain immediately thinks about how it should be ideally. But refactoring that code from its current state to the ideal state sometimes requires an extensive refactoring. At its best, the required refactoring can be time-consuming. At its worst, it can turn into a rabbit hole that causes a big delay on the project.</p>
<p>When I face cases like this, I used to either take a chance and do the extensive refactoring anyway or hold my nose and let the code stay stinky. Now I realize there is a happy middle point: <strong>I don’t have the make the code perfect. I can spend as much or as little time as I can afford to make the code a bit better.</strong></p>
<p>Small improvements I do can be still useful for the developers that come next. And those developers may have time to improve the code further, building upon the improvements I make.</p>
<p>(PS: There’s no perfect nor ideal code. The moment you make a change, you see more improvements you can make.)</p>
<h4 id="heading-time-box-your-refactoring"><strong>Time-box your refactoring.</strong></h4>
<p>Time-boxing is a great technique to avoid rabbit holes. Before starting to refactor, do a quick estimation about how much time you can spend on refactoring legacy code without delaying the project too much. Write that number down.</p>
<p>Time-boxing your refactoring gives you the peace of mind knowing the main project will stay on track. And that makes the refactoring experience more enjoyable.</p>
<h4 id="heading-bake-the-refactoring-time-into-the-project-estimation"><strong>Bake the refactoring time into the project estimation.</strong></h4>
<p>Things can be even easier if you bake the refactoring time into the project estimation before starting a project. While estimating how much time a project might take, take a quick look at the code you need to touch: when was it first written? How readable and extensible is it? How much test coverage does it have? Unless there’s a hard requirement about when the project needs to be shipped, you should bake some refactoring time into the project’s timeline.</p>
<p>After all, <strong>as we keep building upon a legacy codebase, refactoring it as we go is the best way to keep it healthy.</strong> And a healthy codebase, in turn, makes building new features faster and safer.</p>
<h4 id="heading-refactoring-is-educational"><strong>Refactoring is educational.</strong></h4>
<p>Refactoring a piece of code is the best way to understand it. Compared to staying aloof and just reading a piece of code, coming in and modifying it gives you a much deeper and intimate relationship with the code.</p>
<p>When you try to add your touch to the code, you will start to notice things you missed before. You may see the constraints and may even understand why the author originally wrote the code this way.</p>
<p><strong>Although refactoring a piece of code doesn’t seem to add any immediate value to the business, it’s an opportunity for you to gain a piece of domain knowledge you currently don’t have.</strong> Gaining that domain knowledge is invaluable to both you and the business.</p>
<h4 id="heading-refactoring-is-a-muscle-you-can-build"><strong>Refactoring is a muscle you can build</strong></h4>
<p><strong>The more you do it, the faster you become.</strong> One last reason to refactor legacy code is to use it as an opportunity to strengthen your “refactoring muscle”. Investing in building your “refactoring muscle” has a self-reinforcing compound effect: <strong>the more you refactor, the better you are at refactoring, making your next refactoring faster.</strong> The more you do it, the better the codebase is, making building future features faster.</p>
<p>Luckily, my next project relates to code from almost three years ago. I expect to face many cases requiring me to balance between refactoring legacy code and making progress on the project. I will share my new learning on <a target="_blank" href="https://www.sihui.io">my personal blog</a> (NOT medium). Subscribe to follow along!</p>
<p>My career plan for the year is to grow into a tech lead. I’m excited about all the learnings ahead and would love to share this journey with you in a brutally honest fashion. <strong>I will be sharing my weekly learning on my <a target="_blank" href="https://www.sihui.io">personal blog</a> (NOT medium)</strong>.</p>
<p>In the next few months, I will focus on growing in the following areas, so you can expect to see posts related to them:</p>
<ul>
<li>focusing on the big picture of the project instead of near-term implementation details;</li>
<li>balancing my efforts between leading projects and coding;</li>
<li>work-life balance for long-term productivity;</li>
<li>the human side of software development: making sure everyone riding with me enjoys the ride and feels fulfilled and inspired.</li>
</ul>
<p><em>Originally published at <a target="_blank" href="https://www.sihui.io/refactoring-vs-staying-focused/">www.sihui.io</a> on February 1, 2019.</em></p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
