<?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[ software architecture - 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[ software architecture - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 22 Sep 2026 21:39:10 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/software-architecture/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 Use Differential Testing During a Legacy Migration ]]>
                </title>
                <description>
                    <![CDATA[ The most dangerous moment in a legacy migration isn't necessarily when you start writing the new implementation. It's when the new implementation looks finished. The code compiles, the tests pass, the ]]>
                </description>
                <link>https://www.freecodecamp.org/news/differential-testing-legacy-migration/</link>
                <guid isPermaLink="false">6aa8200e59dfce663a0e73bd</guid>
                
                    <category>
                        <![CDATA[ legacy code ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Software Testing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software architecture ]]>
                    </category>
                
                    <category>
                        <![CDATA[ migration ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Artificial Intelligence ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Hugo Teijiz ]]>
                </dc:creator>
                <pubDate>Mon, 14 Sep 2026 16:25:50 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/6717bbb6-16b9-4fc1-8bfe-7381a3024f73.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>The most dangerous moment in a legacy migration isn't necessarily when you start writing the new implementation. It's when the new implementation looks finished.</p>
<p>The code compiles, the tests pass, the architecture is cleaner, and the new service responds faster.</p>
<p>Then everybody starts asking the same question:</p>
<blockquote>
<p>Can we switch traffic now?</p>
</blockquote>
<p>That's where confidence becomes difficult.</p>
<p>A new implementation can pass its own test suite and still behave differently from the system it is replacing.</p>
<p>Maybe rounding changed, or null values are handled differently, or an error became a successful response.</p>
<p>Maybe records are sorted differently, or a side effect happens in a different order, or a business rule you never documented was lost during the migration.</p>
<p>This is why, during a legacy migration, I like having another source of evidence: <strong>run the old and new implementations with the same inputs and compare what they do.</strong></p>
<p>That's the basic idea behind differential testing. Instead of asking only if the new system passes its tests, you also ask: given the same input, where does the new system behave differently from the old one?</p>
<p>Those differences become evidence.</p>
<p>Some are bugs, some are intentional improvements, some are harmless representation differences, and some reveal behavior nobody knew existed.</p>
<p>In this tutorial, I'll show you how to use differential testing during a legacy migration to:</p>
<ul>
<li><p>Compare old and new implementations</p>
</li>
<li><p>Define what should be considered equivalent</p>
</li>
<li><p>Normalize outputs before comparing them</p>
</li>
<li><p>Handle timestamps and other nondeterministic values</p>
</li>
<li><p>Compare errors and side effects</p>
</li>
<li><p>Run differential tests automatically</p>
</li>
<li><p>Introduce tolerances where exact equality doesn't make sense</p>
</li>
<li><p>Analyze mismatches</p>
</li>
<li><p>Use shadow traffic in production safely</p>
</li>
<li><p>Use AI to classify divergences without letting it decide correctness</p>
</li>
<li><p>Determine when the new implementation is ready for cutover</p>
</li>
</ul>
<p>The examples use TypeScript and Vitest, but the approach applies to most languages and migration strategies.</p>
<p>The goal isn't to prove that two implementations are internally identical. It's to obtain evidence that they are <strong>behaviorally equivalent where equivalence matters</strong>.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along here, you should be comfortable with:</p>
<ul>
<li><p>TypeScript or a similar language</p>
</li>
<li><p>unit and integration testing</p>
</li>
<li><p>asynchronous code</p>
</li>
<li><p>API and service boundaries</p>
</li>
<li><p>legacy modernization</p>
</li>
<li><p>basic observability concepts</p>
</li>
</ul>
<p>You should also already have some understanding of the capability being migrated.</p>
<p>Ideally, you know its inputs, outputs, important business rules, external contracts, side effects, and known areas of uncertainty.</p>
<p>Differential testing works best after you've already created a boundary around the capability you want to migrate.</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-what-differential-testing-actually-tells-you">What Differential Testing Actually Tells You</a></p>
</li>
<li><p><a href="#heading-start-with-one-observable-boundary">Start with One Observable Boundary</a></p>
</li>
<li><p><a href="#heading-run-the-legacy-and-new-implementations-with-the-same-input">Run the Legacy and New Implementations with the Same Input</a></p>
</li>
<li><p><a href="#heading-dont-compare-raw-output-blindly">Don't Compare Raw Output Blindly</a></p>
</li>
<li><p><a href="#heading-normalize-values-before-comparing-them">Normalize Values Before Comparing Them</a></p>
</li>
<li><p><a href="#heading-handle-timestamps-and-other-nondeterministic-values">Handle Timestamps and Other Nondeterministic Values</a></p>
</li>
<li><p><a href="#heading-compare-business-meaning-not-just-json">Compare Business Meaning, Not Just JSON</a></p>
</li>
<li><p><a href="#heading-compare-errors-as-part-of-the-contract">Compare Errors as Part of the Contract</a></p>
</li>
<li><p><a href="#heading-compare-side-effects-too">Compare Side Effects, Too</a></p>
</li>
<li><p><a href="#heading-use-tolerances-when-exact-equality-is-wrong">Use Tolerances When Exact Equality Is Wrong</a></p>
</li>
<li><p><a href="#heading-build-a-reusable-differential-test-harness">Build a Reusable Differential Test Harness</a></p>
</li>
<li><p><a href="#heading-generate-test-cases-from-real-behavior">Generate Test Cases from Real Behavior</a></p>
</li>
<li><p><a href="#heading-classify-every-difference">Classify Every Difference</a></p>
</li>
<li><p><a href="#heading-how-to-use-ai-to-investigate-differential-failures">How to Use AI to Investigate Differential Failures</a></p>
</li>
<li><p><a href="#heading-how-to-use-shadow-traffic-safely">How to Use Shadow Traffic Safely</a></p>
</li>
<li><p><a href="#heading-measure-divergence-instead-of-waiting-for-perfection">Measure Divergence Instead of Waiting for Perfection</a></p>
</li>
<li><p><a href="#heading-how-to-know-when-youre-ready-for-cutover">How to Know When You're Ready for Cutover</a></p>
</li>
<li><p><a href="#heading-a-practical-differential-testing-workflow">A Practical Differential Testing Workflow</a></p>
</li>
<li><p><a href="#heading-what-differential-testing-cant-prove">What Differential Testing Can't Prove</a></p>
</li>
<li><p><a href="#heading-differential-testing-turns-migration-risk-into-evidence">Differential Testing Turns Migration Risk into Evidence</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-differential-testing-actually-tells-you">What Differential Testing Actually Tells You</h2>
<p>Imagine that your legacy application calculates the final price of an order.</p>
<p>The legacy implementation looks like this:</p>
<pre><code class="language-typescript">type Order = {
  subtotal: number;
  customerType: "STANDARD" | "PREMIUM";
  country: string;
};

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

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

  if (order.country === "AR") {
    total -= 500;
  }

  return Math.max(total, 0);
}
</code></pre>
<p>During the migration, you create a new implementation:</p>
<pre><code class="language-typescript">function newCalculateTotal(order: Order): number {
  const premiumDiscount =
    order.customerType === "PREMIUM"
      ? order.subtotal * 0.1
      : 0;

  const countryAdjustment =
    order.country === "AR"
      ? 500
      : 0;

  return Math.max(
    order.subtotal -
      premiumDiscount -
      countryAdjustment,
    0
  );
}
</code></pre>
<p>The implementations look different. And that's fine. What matters is whether they produce equivalent behavior.</p>
<p>A simple differential test can run both:</p>
<pre><code class="language-typescript">import { describe, expect, it } from "vitest";

describe("order total migration", () =&gt; {
  it("matches the legacy implementation", () =&gt; {
    const order: Order = {
      subtotal: 10000,
      customerType: "PREMIUM",
      country: "AR",
    };

    const legacy =
      legacyCalculateTotal(order);

    const migrated =
      newCalculateTotal(order);

    expect(migrated).toBe(legacy);
  });
});
</code></pre>
<p>For this input:</p>
<pre><code class="language-text">legacy → 8500
new    → 8500
</code></pre>
<p>Good. But one matching example proves very little.</p>
<p>The value comes from systematically asking:</p>
<pre><code class="language-text">same input
↓
legacy implementation ──→ result A

same input
↓
new implementation ─────→ result B

compare A and B
</code></pre>
<p>Every mismatch gives you something to investigate.</p>
<h2 id="heading-start-with-one-observable-boundary">Start with One Observable Boundary</h2>
<p>Don't begin by comparing entire applications. To start, choose one capability.</p>
<p>For example:</p>
<pre><code class="language-text">Calculate Order Total
Generate Invoice
Approve Customer
Renew Subscription
Calculate Commission
Create Shipment
</code></pre>
<p>Suppose the migration boundary is:</p>
<pre><code class="language-typescript">interface OrderProcessor {
  process(order: Order): Promise&lt;ProcessedOrder&gt;;
}
</code></pre>
<p>Now you have two implementations:</p>
<pre><code class="language-text">LegacyOrderProcessor

NewOrderProcessor
</code></pre>
<p>That is a useful differential boundary, because both receive the same conceptual input, and both produce the same conceptual output.</p>
<p>You can compare them without requiring their internal architecture to match.</p>
<p>That matters because migrations often change structure intentionally.</p>
<p>The legacy implementation might be:</p>
<pre><code class="language-text">controller
→ service
→ SQL
→ provider SDK
</code></pre>
<p>while the new implementation might be:</p>
<pre><code class="language-text">use case
→ repository
→ gateway
→ events
</code></pre>
<p>Differential testing shouldn't care. It should care about observable behavior.</p>
<h2 id="heading-run-the-legacy-and-new-implementations-with-the-same-input">Run the Legacy and New Implementations with the Same Input</h2>
<p>Suppose both implementations expose:</p>
<pre><code class="language-typescript">interface OrderProcessor {
  process(order: Order): Promise&lt;ProcessedOrder&gt;;
}
</code></pre>
<p>You can create:</p>
<pre><code class="language-typescript">const legacyProcessor =
  new LegacyOrderProcessor();

const newProcessor =
  new NewOrderProcessor();
</code></pre>
<p>Then:</p>
<pre><code class="language-typescript">it("produces the same processed order", async () =&gt; {
  const input: Order = {
    id: "order-1",
    subtotal: 10000,
    customerType: "PREMIUM",
    country: "US",
  };

  const legacy =
    await legacyProcessor.process(
      structuredClone(input)
    );

  const migrated =
    await newProcessor.process(
      structuredClone(input)
    );

  expect(migrated).toEqual(legacy);
});
</code></pre>
<p>Notice the use of:</p>
<pre><code class="language-typescript">structuredClone(input)
</code></pre>
<p>That matters if either implementation mutates its input.</p>
<p>Without separate copies, the first execution could influence the second.</p>
<p>You want:</p>
<pre><code class="language-text">same initial state
</code></pre>
<p>not:</p>
<pre><code class="language-text">new implementation receives state modified by legacy implementation
</code></pre>
<p>That kind of contamination can create misleading results.</p>
<h2 id="heading-dont-compare-raw-output-blindly">Don't Compare Raw Output Blindly</h2>
<p>The first version of a differential test is often:</p>
<pre><code class="language-typescript">expect(newResult).toEqual(legacyResult);
</code></pre>
<p>Sometimes that's exactly right. But other times it's wrong.</p>
<p>Imagine the legacy system returns:</p>
<pre><code class="language-json">{
  "id": "order-1",
  "total": 9000,
  "status": "PROCESSED",
  "generatedAt": "2026-09-09T10:00:01.231Z",
  "requestId": "legacy-f93a"
}
</code></pre>
<p>The new system returns:</p>
<pre><code class="language-json">{
  "requestId": "new-b517",
  "status": "PROCESSED",
  "generatedAt": "2026-09-09T10:00:01.416Z",
  "total": 9000,
  "id": "order-1"
}
</code></pre>
<p>A raw object comparison may fail because:</p>
<pre><code class="language-text">requestId differs
timestamp differs
</code></pre>
<p>But the business behavior might be equivalent.</p>
<p>You need to decide which fields are part of the meaningful contract.</p>
<p>Maybe:</p>
<pre><code class="language-text">id
total
status
</code></pre>
<p>matter.</p>
<p>While:</p>
<pre><code class="language-text">generatedAt
requestId
</code></pre>
<p>don't need exact equivalence.</p>
<p>That leads to normalization.</p>
<h2 id="heading-normalize-values-before-comparing-them">Normalize Values Before Comparing Them</h2>
<p>Normalization means transforming outputs into a common representation before comparing them.</p>
<p>The goal isn't to change the business meaning of the data. It's to remove differences that are expected and irrelevant to the comparison, such as generated request IDs or timestamps, so the test can focus on the fields that actually define the behavior you care about.</p>
<p>In practice, that often means creating a canonical representation: a smaller, stable shape that contains only the meaningful fields you want to compare.</p>
<p>For example:</p>
<pre><code class="language-typescript">type ProcessedOrder = {
  id: string;
  total: number;
  status: string;
  generatedAt: string;
  requestId: string;
};

function normalizeOrder(
  order: ProcessedOrder
) {
  return {
    id: order.id,
    total: order.total,
    status: order.status,
  };
}
</code></pre>
<p>Here, <code>ProcessedOrder</code> contains both business-relevant fields and values that may legitimately differ between executions.</p>
<p>The <code>normalizeOrder()</code> function keeps <code>id</code>, <code>total</code>, and <code>status</code>, while leaving out <code>generatedAt</code> and <code>requestId</code>. That means two results can still be considered equivalent even if they were generated at slightly different times or used different request identifiers.</p>
<p>Now compare:</p>
<pre><code class="language-typescript">expect(
  normalizeOrder(migrated)
).toEqual(
  normalizeOrder(legacy)
);
</code></pre>
<p>This makes your equivalence rule explicit.</p>
<p>You're saying:</p>
<blockquote>
<p>These fields define relevant behavior for this comparison.</p>
</blockquote>
<p>Normalization can also handle:</p>
<ul>
<li><p>ordering</p>
</li>
<li><p>casing</p>
</li>
<li><p>optional fields</p>
</li>
<li><p>timestamps</p>
</li>
<li><p>generated identifiers</p>
</li>
<li><p>numeric formatting</p>
</li>
<li><p>provider-specific metadata</p>
</li>
</ul>
<p>But normalization must be deliberate. If you remove too much, you can hide real migration bugs.</p>
<h2 id="heading-handle-timestamps-and-other-nondeterministic-values">Handle Timestamps and Other Nondeterministic Values</h2>
<p>Legacy systems contain many nondeterministic values.</p>
<p>For example:</p>
<pre><code class="language-text">timestamps
UUIDs
random tokens
request IDs
trace IDs
database-generated IDs
unordered collections
provider-generated references
</code></pre>
<p>If you compare those values exactly, your differential suite may fail constantly.</p>
<p>One option is dependency control.</p>
<p>Dependency control means moving a nondeterministic source, such as the current time or an ID generator, behind an interface that you can replace during tests.</p>
<p>Instead of letting each implementation read the real clock independently, you inject the same controlled clock into both. That gives them the same value and removes time itself as a source of meaningless divergence.</p>
<p>Suppose the code uses:</p>
<pre><code class="language-typescript">new Date()
</code></pre>
<p>You can replace that dependency with a clock:</p>
<pre><code class="language-typescript">interface Clock {
  now(): Date;
}
</code></pre>
<p>Then both implementations receive:</p>
<pre><code class="language-typescript">const clock = {
  now: () =&gt;
    new Date(
      "2026-09-09T10:00:00.000Z"
    ),
};
</code></pre>
<p>Now time becomes deterministic.</p>
<p>The same technique can work for ID generation:</p>
<pre><code class="language-typescript">interface IdGenerator {
  next(): string;
}
</code></pre>
<p>Then tests can provide:</p>
<pre><code class="language-typescript">const ids = {
  next: () =&gt; "fixed-id",
};
</code></pre>
<p>If controlling nondeterminism is impractical, normalize it out only when it's not part of the behavior you need to protect.</p>
<h2 id="heading-compare-business-meaning-not-just-json">Compare Business Meaning, Not Just JSON</h2>
<p>Two systems can return different representations while expressing the same business state.</p>
<p>Imagine you have this in your legacy system:</p>
<pre><code class="language-json">{
  "status": 2
}
</code></pre>
<p>And this in your new one:</p>
<pre><code class="language-json">{
  "status": "APPROVED"
}
</code></pre>
<p>Raw comparison says:</p>
<pre><code class="language-text">different
</code></pre>
<p>Business comparison may say:</p>
<pre><code class="language-text">equivalent
</code></pre>
<p>You can create a semantic normalizer:</p>
<pre><code class="language-typescript">function normalizeStatus(
  status: number | string
) {
  if (status === 2) {
    return "APPROVED";
  }

  return status;
}
</code></pre>
<p>Here, the normalizer translates the legacy numeric value <code>2</code> into the business meaning used by the new implementation: <code>"APPROVED"</code>.</p>
<p>It doesn't claim that every number and string are interchangeable. It encodes one explicit equivalence rule that you've already decided is valid for this migration.</p>
<p>Then:</p>
<pre><code class="language-typescript">expect(
  normalizeStatus(newResult.status)
).toBe(
  normalizeStatus(legacyResult.status)
);
</code></pre>
<p>This is especially useful when migration intentionally changes:</p>
<pre><code class="language-text">database schema
API representation
enumerations
provider-specific formats
internal identifiers
</code></pre>
<p>The important question becomes:</p>
<blockquote>
<p>Does the observable business meaning remain equivalent?</p>
</blockquote>
<p>Not:</p>
<blockquote>
<p>Are the bytes identical?</p>
</blockquote>
<h2 id="heading-compare-errors-as-part-of-the-contract">Compare Errors as Part of the Contract</h2>
<p>Success responses aren't the whole behavior. Errors matter too.</p>
<p>Suppose the legacy implementation rejects a missing customer:</p>
<pre><code class="language-typescript">throw new Error("Customer not found");
</code></pre>
<p>The new implementation accidentally returns:</p>
<pre><code class="language-typescript">return null;
</code></pre>
<p>These two implementations behave very differently for the same invalid input.</p>
<p>The legacy version fails explicitly, while the new version silently returns a value that a caller may interpret as a successful result.</p>
<p>If your differential tests only exercise cases where a valid customer exists, both implementations may appear equivalent and this contract change will remain invisible.</p>
<p>That's why failure behavior has to be compared too.</p>
<p>Create cases that capture errors:</p>
<pre><code class="language-typescript">async function captureResult&lt;T&gt;(
  operation: () =&gt; Promise&lt;T&gt;
) {
  try {
    return {
      type: "success" as const,
      value: await operation(),
    };
  } catch (error) {
    return {
      type: "error" as const,
      error:
        error instanceof Error
          ? error.message
          : String(error),
    };
  }
}
</code></pre>
<p>The helper wraps an asynchronous operation and converts both possible outcomes into data.</p>
<p>If the operation succeeds, it returns an object with <code>type: "success"</code> and the returned value. If the operation throws, the <code>catch</code> block converts that exception into an object with <code>type: "error"</code> and a readable error message.</p>
<p>This gives both implementations the same comparison shape, so the test can compare success versus failure explicitly instead of letting an exception stop the test before the two behaviors can be evaluated.</p>
<p>Now:</p>
<pre><code class="language-typescript">const legacy =
  await captureResult(() =&gt;
    legacyProcessor.process(input)
  );

const migrated =
  await captureResult(() =&gt;
    newProcessor.process(input)
  );

expect(migrated.type).toBe(legacy.type);
</code></pre>
<p>If errors are contractually important, compare:</p>
<pre><code class="language-text">error category
HTTP status
error code
retryability
validation details
</code></pre>
<p>Don't necessarily compare exact wording unless clients depend on it.</p>
<h2 id="heading-compare-side-effects-too">Compare Side Effects, Too</h2>
<p>One of the easiest migration mistakes is preserving the return value while losing a side effect.</p>
<p>Suppose both implementations return:</p>
<pre><code class="language-json">{
  "status": "PROCESSED"
}
</code></pre>
<p>But the legacy version also:</p>
<pre><code class="language-text">persists the order
publishes an event
creates a payment
writes an audit entry
</code></pre>
<p>and the new version forgets the audit entry.</p>
<p>Response-level differential testing won't catch that. So you'll want to capture side effects.</p>
<p>For example:</p>
<pre><code class="language-typescript">type Effect =
  | {
      type: "payment";
      orderId: string;
      amount: number;
    }
  | {
      type: "event";
      name: string;
      orderId: string;
    };
</code></pre>
<p>A test adapter can record them:</p>
<pre><code class="language-typescript">class RecordingPaymentGateway {
  effects: Effect[] = [];

  async charge(
    orderId: string,
    amount: number
  ) {
    this.effects.push({
      type: "payment",
      orderId,
      amount,
    });
  }
}
</code></pre>
<p>Instead of sending a real payment request, this adapter records what the application attempted to do in the <code>effects</code> array.</p>
<p>You can apply the same idea to event publication:</p>
<pre><code class="language-typescript">class RecordingEvents {
  effects: Effect[] = [];

  async publish(
    name: string,
    orderId: string
  ) {
    this.effects.push({
      type: "event",
      name,
      orderId,
    });
  }
}
</code></pre>
<p>The application still calls its payment and event dependencies as usual. The test doubles simply capture those calls as structured data instead of performing the real external actions.</p>
<p>After running the legacy and migrated implementations with their own recording adapters, you can compare the two recorded effect lists and verify that both systems attempted the same observable side effects.</p>
<p>Now the differential test can compare:</p>
<pre><code class="language-typescript">expect(newEffects).toEqual(legacyEffects);
</code></pre>
<p>Again, exact ordering should only be required if ordering matters.</p>
<h2 id="heading-use-tolerances-when-exact-equality-is-wrong">Use Tolerances When Exact Equality Is Wrong</h2>
<p>Some domains shouldn't use exact equality.</p>
<p>Imagine a migrated calculation produces:</p>
<pre><code class="language-text">legacy → 34.333333333
new    → 34.333333334
</code></pre>
<p>Is that a migration bug? Maybe not.</p>
<p>Floating-point calculations may justify a tolerance.</p>
<p>For example:</p>
<pre><code class="language-typescript">expect(newResult).toBeCloseTo(
  legacyResult,
  6
);
</code></pre>
<p>Or define an explicit comparator:</p>
<pre><code class="language-typescript">function withinTolerance(
  a: number,
  b: number,
  tolerance: number
) {
  return Math.abs(a - b) &lt;= tolerance;
}
</code></pre>
<p>Then:</p>
<pre><code class="language-typescript">expect(
  withinTolerance(
    migrated.total,
    legacy.total,
    0.01
  )
).toBe(true);
</code></pre>
<p>But tolerances should come from domain requirements. Don't use them just to make failing tests disappear.</p>
<p>For financial systems, one cent can matter. For scientific calculations, a much smaller numerical difference may matter.</p>
<p>Equivalence is a business and engineering decision.</p>
<h2 id="heading-build-a-reusable-differential-test-harness">Build a Reusable Differential Test Harness</h2>
<p>Once you compare more than a few cases, you can create a reusable harness.</p>
<p>For example:</p>
<pre><code class="language-typescript">type DifferentialResult&lt;T&gt; = {
  input: T;
  equivalent: boolean;
  legacy: unknown;
  migrated: unknown;
};

async function compareImplementations&lt;
  TInput,
  TOutput
&gt;(
  input: TInput,
  legacy: (
    input: TInput
  ) =&gt; Promise&lt;TOutput&gt;,
  migrated: (
    input: TInput
  ) =&gt; Promise&lt;TOutput&gt;,
  normalize: (
    output: TOutput
  ) =&gt; unknown
): Promise&lt;
  DifferentialResult&lt;TInput&gt;
&gt; {
  const legacyResult =
    await legacy(
      structuredClone(input)
    );

  const migratedResult =
    await migrated(
      structuredClone(input)
    );

  const normalizedLegacy =
    normalize(legacyResult);

  const normalizedMigrated =
    normalize(migratedResult);

  return {
    input,
    equivalent:
      JSON.stringify(
        normalizedLegacy
      ) ===
      JSON.stringify(
        normalizedMigrated
      ),
    legacy: normalizedLegacy,
    migrated: normalizedMigrated,
  };
}
</code></pre>
<p>The harness does four things.</p>
<p>First, it runs the legacy and migrated implementations with separate clones of the same input, so one execution can't mutate the data seen by the other.</p>
<p>Second, it passes both outputs through the same <code>normalize()</code> function. That applies the equivalence rules in one place instead of repeating them in every test.</p>
<p>Third, it compares the normalized results and records whether they're equivalent.</p>
<p>Finally, it returns the input and both normalized outputs together. That makes a failed comparison easier to inspect because the test report can show exactly which case diverged and what each implementation produced.</p>
<p>Then:</p>
<pre><code class="language-typescript">const result =
  await compareImplementations(
    input,
    legacyProcessor.process.bind(
      legacyProcessor
    ),
    newProcessor.process.bind(
      newProcessor
    ),
    normalizeOrder
  );

expect(result.equivalent).toBe(true);
</code></pre>
<p>For real systems, I would usually avoid relying on <code>JSON.stringify()</code> as the final equality mechanism.</p>
<p>The example keeps the harness readable.</p>
<p>In production-quality tooling, use a proper structural or domain-specific comparator.</p>
<p>The important part is that comparison logic becomes centralized.</p>
<h2 id="heading-generate-test-cases-from-real-behavior">Generate Test Cases from Real Behavior</h2>
<p>Hand-written examples are useful. But migrations often fail on cases nobody thought to write manually.</p>
<p>Useful sources of inputs include:</p>
<pre><code class="language-text">existing test fixtures
historical incidents
production-safe request samples
database records
boundary values
previous bug reports
known customer scenarios
</code></pre>
<p>Suppose production shows these order shapes:</p>
<pre><code class="language-typescript">const cases: Order[] = [
  {
    subtotal: 0,
    customerType: "STANDARD",
    country: "US",
  },
  {
    subtotal: 500,
    customerType: "PREMIUM",
    country: "AR",
  },
  {
    subtotal: 10000,
    customerType: "STANDARD",
    country: "AR",
  },
];
</code></pre>
<p>The first block is the test data. It captures a small set of representative input shapes that you've observed in real usage or reconstructed safely from production behavior.</p>
<p>The next block is the test itself. <code>it.each(cases)</code> tells Vitest to run the same differential comparison once for every input in that array.</p>
<p>That separates two concerns: defining realistic cases and defining how every case should be evaluated.</p>
<p>Now:</p>
<pre><code class="language-typescript">it.each(cases)(
  "matches legacy behavior",
  async (input) =&gt; {
    const legacy =
      await legacyProcessor.process(
        structuredClone(input)
      );

    const migrated =
      await newProcessor.process(
        structuredClone(input)
      );

    expect(
      normalizeOrder(migrated)
    ).toEqual(
      normalizeOrder(legacy)
    );
  }
);
</code></pre>
<p>Real examples help expose assumptions that synthetic test data often misses. But production data must be handled carefully.</p>
<p>Remove or anonymize:</p>
<pre><code class="language-text">personal data
credentials
tokens
financial identifiers
confidential business data
</code></pre>
<p>The objective is to preserve useful behavioral shapes, not copy sensitive production information into test fixtures.</p>
<h2 id="heading-classify-every-difference">Classify Every Difference</h2>
<p>A differential failure doesn't automatically mean that the new implementation is wrong.</p>
<p>Suppose you find 200 mismatches. Classify them.</p>
<p>I like categories such as:</p>
<pre><code class="language-text">migration defect
legacy defect intentionally preserved
intentional behavior change
representation difference
nondeterministic difference
test/comparator defect
unknown
</code></pre>
<p>For example:</p>
<pre><code class="language-text">Input:
subtotal = 5000

Legacy:
discount = 0

New:
discount = 500

Classification:
unknown
</code></pre>
<p>Investigation reveals that the new implementation changed:</p>
<pre><code class="language-typescript">amount &gt; 5000
</code></pre>
<p>to:</p>
<pre><code class="language-typescript">amount &gt;= 5000
</code></pre>
<p>Now you need a decision.</p>
<p>Was that:</p>
<pre><code class="language-text">accidental migration change
</code></pre>
<p>or:</p>
<pre><code class="language-text">intentional bug fix
</code></pre>
<p>Differential testing exposes the decision. It doesn't make the decision for you.</p>
<p>That's one of its greatest benefits.</p>
<h2 id="heading-how-to-use-ai-to-investigate-differential-failures">How to Use AI to Investigate Differential Failures</h2>
<p>Large migrations can produce hundreds or thousands of differences. And AI can help triage them.</p>
<p>Suppose you have:</p>
<pre><code class="language-json">{
  "input": {
    "subtotal": 5000,
    "country": "AR"
  },
  "legacy": {
    "total": 4500
  },
  "new": {
    "total": 4000
  }
}
</code></pre>
<p>You can give the model:</p>
<ul>
<li><p>the input</p>
</li>
<li><p>both outputs</p>
</li>
<li><p>relevant legacy code</p>
</li>
<li><p>relevant migrated code</p>
</li>
<li><p>the comparator rules</p>
</li>
</ul>
<p>Then ask:</p>
<pre><code class="language-text">Analyze this differential test failure.

Identify the smallest behavioral difference that could
explain the mismatch.

Compare the legacy and migrated implementations.

Return:

1. observed difference,
2. relevant legacy branch,
3. relevant migrated branch,
4. likely cause,
5. evidence supporting the cause,
6. additional test cases that could confirm it.

Do not decide which behavior is correct.
Do not modify the code yet.
</code></pre>
<p>That last instruction matters. AI can be very useful for locating why two implementations diverge. It shouldn't silently turn that diagnosis into a business decision.</p>
<h3 id="heading-dont-let-ai-decide-which-behavior-is-correct">Don't Let AI Decide Which Behavior Is Correct</h3>
<p>Imagine the legacy system does this:</p>
<pre><code class="language-text">Customer age 65 → no discount
Customer age 66 → discount
</code></pre>
<p>The new system does:</p>
<pre><code class="language-text">Customer age 65 → discount
Customer age 66 → discount
</code></pre>
<p>AI may look at the code and say:</p>
<blockquote>
<p>The new implementation appears more logical because senior discounts typically begin at age 65.</p>
</blockquote>
<p>That's irrelevant.</p>
<p>The business rule might be:</p>
<pre><code class="language-text">age &gt; 65
</code></pre>
<p>for a reason. Or the legacy behavior might contain a bug.</p>
<p>You need evidence.</p>
<p>Use:</p>
<pre><code class="language-text">requirements
existing tests
production behavior
business owners
historical tickets
commit history
contracts
</code></pre>
<p>AI can help gather and summarize that evidence. It shouldn't invent the rule.</p>
<p>Differential testing is valuable because it tells you that there's a difference before you accidentally turn that difference into production behavior.</p>
<h2 id="heading-how-to-use-shadow-traffic-safely">How to Use Shadow Traffic Safely</h2>
<p>Once offline differential tests look good, you can sometimes compare behavior with real traffic. This is often called shadowing or traffic mirroring.</p>
<p>The pattern looks like:</p>
<pre><code class="language-text">real request
    │
    ├────────────→ legacy system
    │                  │
    │                  ↓
    │             real response
    │
    └────────────→ new system
                       │
                       ↓
                  shadow result
</code></pre>
<p>The user still receives:</p>
<pre><code class="language-text">legacy response
</code></pre>
<p>while the new system processes a copy of the request.</p>
<p>Then you compare:</p>
<pre><code class="language-text">legacy output
vs.
shadow output
</code></pre>
<p>This can reveal cases that your test suite never captured.</p>
<p>For example:</p>
<pre><code class="language-text">unexpected null combinations
rare customer states
unusual international data
old records
large values
unusual sequence patterns
</code></pre>
<p>But shadow execution requires careful design, especially when the operation has side effects.</p>
<h3 id="heading-how-to-prevent-shadow-execution-from-duplicating-side-effects">How to Prevent Shadow Execution from Duplicating Side Effects</h3>
<p>Imagine shadowing:</p>
<pre><code class="language-text">POST /payments
</code></pre>
<p>If both systems really execute the payment, you have a serious problem.</p>
<p>The same applies to:</p>
<pre><code class="language-text">send email
create shipment
charge card
modify inventory
publish event
write external record
</code></pre>
<p>The shadow implementation shouldn't perform destructive or externally visible effects unless they're safely isolated.</p>
<p>One approach is to replace real gateways with recording adapters:</p>
<pre><code class="language-typescript">class ShadowPaymentGateway
  implements PaymentGateway {
  calls: PaymentRequest[] = [];

  async charge(
    request: PaymentRequest
  ) {
    this.calls.push(request);

    return {
      paymentId: "shadow",
    };
  }
}
</code></pre>
<p>The new implementation still tries to execute:</p>
<pre><code class="language-text">payment
</code></pre>
<p>but instead of charging a real card, the shadow adapter records:</p>
<pre><code class="language-text">what would have been sent
</code></pre>
<p>You can then compare that intent with the legacy side effect.</p>
<p>This distinction is important:</p>
<pre><code class="language-text">compare behavior
</code></pre>
<p>does not mean:</p>
<pre><code class="language-text">duplicate production effects
</code></pre>
<h2 id="heading-measure-divergence-instead-of-waiting-for-perfection">Measure Divergence Instead of Waiting for Perfection</h2>
<p>When running thousands of comparisons, a binary:</p>
<pre><code class="language-text">pass / fail
</code></pre>
<p>may not tell the whole story.</p>
<p>You can measure divergence.</p>
<p>For example:</p>
<pre><code class="language-text">Requests compared:     100,000
Equivalent:             99,620
Different:                 380

Divergence rate:          0.38%
</code></pre>
<p>Then classify those 380:</p>
<pre><code class="language-text">250 timestamp differences
80 known intentional changes
30 comparator problems
15 migration defects fixed
5 still unexplained
</code></pre>
<p>After normalization:</p>
<pre><code class="language-text">meaningful unresolved divergence:
5 / 100,000
= 0.005%
</code></pre>
<p>Now the conversation becomes much more concrete.</p>
<p>Instead of:</p>
<blockquote>
<p>I think the migration is ready.</p>
</blockquote>
<p>you can say:</p>
<blockquote>
<p>We compared 100,000 representative executions and have five unresolved behavioral differences.</p>
</blockquote>
<p>Whether that's acceptable depends on what those five cases are.</p>
<p>One incorrect financial transaction can matter more than 100 harmless formatting differences.</p>
<p>So don't evaluate only the percentage. Evaluate the severity.</p>
<h2 id="heading-how-to-know-when-youre-ready-for-cutover">How to Know When You're Ready for Cutover</h2>
<p>Differential testing doesn't give you a universal threshold. But it can give you evidence.</p>
<p>Before cutover, I would want to answer questions such as:</p>
<h3 id="heading-have-important-input-classes-been-compared">Have Important Input Classes Been Compared?</h3>
<p>Not only happy paths.</p>
<p>Include:</p>
<pre><code class="language-text">boundaries
errors
historical bugs
large values
missing values
rare states
</code></pre>
<h3 id="heading-are-meaningful-differences-classified">Are Meaningful Differences Classified?</h3>
<p>Avoid:</p>
<pre><code class="language-text">we have 47 unexplained mismatches
</code></pre>
<h3 id="heading-are-critical-differences-resolved">Are Critical Differences Resolved?</h3>
<p>Especially:</p>
<pre><code class="language-text">money
authorization
state transitions
data integrity
external contracts
idempotency
</code></pre>
<h3 id="heading-are-intentional-differences-documented">Are Intentional Differences Documented?</h3>
<p>If the new behavior intentionally differs, that should be explicit.</p>
<h3 id="heading-are-side-effects-equivalent">Are Side Effects Equivalent?</h3>
<p>Not only responses.</p>
<h3 id="heading-have-production-like-cases-been-tested">Have Production-like Cases Been Tested?</h3>
<p>Synthetic fixtures alone may not be enough.</p>
<h3 id="heading-can-the-migration-be-rolled-back">Can the Migration Be Rolled Back?</h3>
<p>Differential confidence reduces risk. It doesn't eliminate the need for rollback.</p>
<p>If you can answer these questions, you're much closer to a controlled cutover.</p>
<h2 id="heading-a-practical-differential-testing-workflow">A Practical Differential Testing Workflow</h2>
<p>Here's the workflow I would use.</p>
<h3 id="heading-1-pick-one-capability">1. Pick One Capability</h3>
<p>For example:</p>
<pre><code class="language-text">Process Order
Calculate Invoice
Approve Customer
</code></pre>
<p>Don't compare the whole platform at once.</p>
<h3 id="heading-2-define-the-observable-contract">2. Define the Observable Contract</h3>
<p>List what matters:</p>
<pre><code class="language-text">return value
status
error
database state
events
external calls
</code></pre>
<h3 id="heading-3-create-legacy-and-new-adapters">3. Create Legacy and New Adapters</h3>
<p>Expose both implementations through the same conceptual interface.</p>
<h3 id="heading-4-define-normalization-rules">4. Define Normalization Rules</h3>
<p>Decide how to handle:</p>
<pre><code class="language-text">timestamps
generated IDs
ordering
representation changes
optional values
</code></pre>
<p>Do this before looking at lots of failures. Otherwise you may weaken the comparator simply to make results pass.</p>
<h3 id="heading-5-compare-known-cases">5. Compare Known Cases</h3>
<p>Begin with:</p>
<pre><code class="language-text">existing tests
characterization cases
edge cases
historical bugs
</code></pre>
<h3 id="heading-6-capture-side-effects">6. Capture Side Effects</h3>
<p>Use recording or fake adapters where necessary.</p>
<h3 id="heading-7-automate-the-harness">7. Automate the Harness</h3>
<p>Produce structured output for every mismatch.</p>
<p>For example:</p>
<pre><code class="language-json">{
  "caseId": "case-493",
  "equivalent": false,
  "legacy": {},
  "migrated": {},
  "difference": {}
}
</code></pre>
<h3 id="heading-8-classify-differences">8. Classify Differences</h3>
<p>Use categories:</p>
<pre><code class="language-text">defect
intentional change
normalization issue
nondeterminism
unknown
</code></pre>
<h3 id="heading-9-add-representative-real-world-cases">9. Add Representative Real-World Cases</h3>
<p>Use anonymized or safely reconstructed production patterns.</p>
<h3 id="heading-10-shadow-real-traffic-when-appropriate">10. Shadow Real Traffic When Appropriate</h3>
<p>Only after controlling side effects and privacy risk.</p>
<h3 id="heading-11-measure-divergence">11. Measure Divergence</h3>
<p>Track both:</p>
<pre><code class="language-text">frequency
severity
</code></pre>
<h3 id="heading-12-resolve-unknowns-before-cutover">12. Resolve Unknowns Before Cutover</h3>
<p>The most dangerous category is often not:</p>
<pre><code class="language-text">different
</code></pre>
<p>It is:</p>
<pre><code class="language-text">different and nobody knows why
</code></pre>
<h2 id="heading-what-differential-testing-cant-prove">What Differential Testing Can't Prove</h2>
<p>Differential testing has an important limitation: it compares the new system against the old one.</p>
<p>That means the legacy system becomes a behavioral reference. But the legacy system may already be wrong.</p>
<p>Suppose:</p>
<pre><code class="language-text">legacy output = wrong
new output    = same wrong result
</code></pre>
<p>The differential test passes, but that doesn't make the behavior correct.</p>
<p>This is why differential testing should complement:</p>
<pre><code class="language-text">specification tests
characterization tests
business requirements
security testing
performance testing
contract testing
domain review
</code></pre>
<p>It answers:</p>
<blockquote>
<p>Did behavior change?</p>
</blockquote>
<p>It doesn't automatically answer:</p>
<blockquote>
<p>Is this the right behavior?</p>
</blockquote>
<p>That distinction matters. The legacy application is evidence, it's not absolute truth.</p>
<h2 id="heading-differential-testing-turns-migration-risk-into-evidence">Differential Testing Turns Migration Risk into Evidence</h2>
<p>There's another reason I like this technique. Without differential testing, migration discussions can become subjective.</p>
<p>One person says:</p>
<blockquote>
<p>The new implementation looks ready.</p>
</blockquote>
<p>Another says:</p>
<blockquote>
<p>I do not trust it yet.</p>
</blockquote>
<p>Both may have reasonable instincts, but neither statement is very measurable.</p>
<p>Differential testing changes the conversation.</p>
<p>Now you can say:</p>
<pre><code class="language-text">12,000 cases compared
47 differences found
31 representation differences
9 intentional behavior changes
6 migration defects fixed
1 unresolved
</code></pre>
<p>That is a much better engineering discussion. You're converting uncertainty into observable differences. Then you can decide what to do with them.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>A legacy migration is not complete because the new implementation passes its own tests.</p>
<p>The harder question is whether it preserves the behavior that matters from the system it is replacing.</p>
<p>Differential testing gives you another way to answer that question.</p>
<p>Run both implementations with the same inputs.</p>
<p>Compare outputs.</p>
<p>Compare errors.</p>
<p>Compare side effects.</p>
<p>Normalize only the differences that truly do not matter.</p>
<p>Investigate everything else.</p>
<p>And when possible, use representative production behavior to discover cases your test suite did not anticipate.</p>
<p>The migration sequence now becomes:</p>
<pre><code class="language-text">Understand
↓
Characterize
↓
Refactor
↓
Migrate
↓
Compare
↓
Cut over
</code></pre>
<p>AI can accelerate this process too.</p>
<p>It can help build comparators, analyze failures, group similar divergences, inspect code paths, and suggest additional test cases.</p>
<p>But it should not decide which implementation is correct.</p>
<p>That still requires evidence, domain knowledge, and engineering judgment.</p>
<p>The purpose of differential testing is not to eliminate uncertainty completely.</p>
<p>It is to make uncertainty visible <strong>before</strong> you switch production traffic.</p>
<p>Because during a migration, discovering that the new system behaves differently is useful.</p>
<p>Discovering it after the old system has been turned off is much more expensive.</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[ The Design Patterns Handbook: Learn Popular Design Patterns with C# Code Examples ]]>
                </title>
                <description>
                    <![CDATA[ Design patterns are reusable solutions to common problems in software design. Think of them as blueprints: not finished code, but proven templates you can adapt to solve a specific problem in your own ]]>
                </description>
                <link>https://www.freecodecamp.org/news/the-design-patterns-handbook-learn-popular-design-patterns-with-c-code-examples/</link>
                <guid isPermaLink="false">6a9eec68a0d0c091f35da7b8</guid>
                
                    <category>
                        <![CDATA[ design patterns ]]>
                    </category>
                
                    <category>
                        <![CDATA[ C ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software architecture ]]>
                    </category>
                
                    <category>
                        <![CDATA[ handbook ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Isaiah Clifford Opoku ]]>
                </dc:creator>
                <pubDate>Mon, 07 Sep 2026 16:55:04 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/4b19ec2c-2756-44d4-9a96-a5f196fdaae3.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Design patterns are <strong>r</strong>eusable solutions to common problems in software design. Think of them as blueprints: not finished code, but proven templates you can adapt to solve a specific problem in your own codebase.</p>
<p>This handbook serves as a practical guide to understanding software design patterns. I wrote it for every developer, regardless of the language you program in. Examples are written in C#, but every concept here applies equally to Python, Java, TypeScript, Go, and beyond.</p>
<p>The source code lives at <a href="https://github.com/Clifftech123/design-patterns-handbook">github.com/Clifftech123/design-patterns-handbook</a>.</p>
<h3 id="heading-things-to-keep-in-mind">Things to Keep in Mind:</h3>
<ul>
<li><p><strong>Design patterns aren't code.</strong> They're a way of <em>thinking</em> about how to structure your code. They're a tool, not a silver bullet, for solving specific design problems.</p>
</li>
<li><p><strong>The concepts are universal.</strong> The examples here are written in C#, but the same patterns exist in every language. If you write Python, Java, Go, or TypeScript, you're already using some of these without knowing it.</p>
</li>
<li><p><strong>There's no one-size-fits-all pattern.</strong> Each pattern exists to address a particular kind of problem. Understanding <em>what problem a pattern solves</em> is more important than memorizing the implementation.</p>
</li>
</ul>
<p>I use C# here as the teaching language because it's clear, readable, and widely understood. The goal of this handbook is for you to walk away understanding the pattern itself, not just the C# code.</p>
<p>There are three main types of design patterns: Creational, Structural, and Behavioral. We'll look at each one in turn here, starting with Creational.</p>
<h3 id="heading-what-well-cover">What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-things-to-keep-in-mind">Things to Keep in Mind</a></p>
</li>
<li><p><a href="#heading-creational-design-patterns">Creational Design Patterns</a></p>
<ul>
<li><p><a href="#heading-1-singleton-design-pattern">1. Singleton Design Pattern</a></p>
</li>
<li><p><a href="#heading-2-the-factory-method">2. The Factory Method</a></p>
</li>
<li><p><a href="#heading-3-the-abstract-factory-design-pattern">3. The Abstract Factory Design Pattern</a></p>
</li>
<li><p><a href="#heading-4-the-builder-design-pattern">4. The Builder Design Pattern</a></p>
</li>
<li><p><a href="#heading-5-the-prototype-design-pattern">5. The Prototype Design Pattern</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-structural-design-patterns">Structural Design Patterns</a></p>
<ul>
<li><p><a href="#heading-1-the-adapter-design-pattern">1. The Adapter Design Pattern</a></p>
</li>
<li><p><a href="#heading-2-the-bridge-design-pattern">2. The Bridge Design Pattern</a></p>
</li>
<li><p><a href="#heading-3-the-composite-design-pattern">3. The Composite Design Pattern</a></p>
</li>
<li><p><a href="#heading-4-the-decorator-design-pattern">4. The Decorator Design Pattern</a></p>
</li>
<li><p><a href="#heading-5-the-facade-design-pattern">5. The Facade Design Pattern</a></p>
</li>
<li><p><a href="#heading-6-the-flyweight-design-pattern">6. The Flyweight Design Pattern</a></p>
</li>
<li><p><a href="#heading-7-the-proxy-design-pattern">7. The Proxy Design Pattern</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-behavioral-design-patterns">Behavioral Design Patterns</a></p>
<ul>
<li><p><a href="#heading-1-the-chain-of-responsibility-design-pattern">1. The Chain of Responsibility Design Pattern</a></p>
</li>
<li><p><a href="#heading-2-the-command-design-pattern">2. The Command Design Pattern</a></p>
</li>
<li><p><a href="#heading-3-the-interpreter-design-pattern">3. The Interpreter Design Pattern</a></p>
</li>
<li><p><a href="#heading-4-the-iterator-design-pattern">4. The Iterator Design Pattern</a></p>
</li>
<li><p><a href="#heading-5-the-mediator-design-pattern">5. The Mediator Design Pattern</a></p>
</li>
<li><p><a href="#heading-6-the-memento-design-pattern">6. The Memento Design Pattern</a></p>
</li>
<li><p><a href="#heading-7-the-observer-design-pattern">7. The Observer Design Pattern</a></p>
</li>
<li><p><a href="#heading-8-the-state-design-pattern">8. The State Design Pattern</a></p>
</li>
<li><p><a href="#heading-9-the-strategy-design-pattern">9. The Strategy Design Pattern</a></p>
</li>
<li><p><a href="#heading-10-the-template-method-design-pattern">10. The Template Method Design Pattern</a></p>
</li>
<li><p><a href="#heading-11-the-visitor-design-pattern">11. The Visitor Design Pattern</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
<ul>
<li><a href="#heading-a-few-things-worth-remembering">A few things worth remembering</a></li>
</ul>
</li>
</ul>
<h2 id="heading-creational-design-patterns">Creational Design Patterns</h2>
<p>Simply put, Creational patterns are all about <strong>how objects are created</strong>. They can be divided into class-creation patterns, which use inheritance to decide which class to instantiate, and object-creation patterns, which use delegation to get the job done.</p>
<p>Wikipedia describes them as:</p>
<blockquote>
<p><em>"A creational pattern aims to separate a system from how its objects are created, composed, and represented. They increase the system's flexibility in terms of the what, who, how, and when of object creation."</em></p>
<p><strong>(</strong><a href="https://en.wikipedia.org/wiki/Creational_pattern"><strong>Source</strong></a><strong>)</strong></p>
</blockquote>
<p>So Creational patterns keep the details of object creation <strong>hidden from the client code</strong>, making the system easier to manage and maintain.</p>
<p>They also abstract away how objects are created, composed, and represented, so the rest of your code doesn't need to care.</p>
<p>There are five Creational design patterns, which we'll go over one by one below:</p>
<ol>
<li><p><strong>Singleton</strong>: Ensures a class has only one instance and provides a global point of access to it.</p>
</li>
<li><p><strong>Factory Method</strong>: Defines an interface for creating an object, but lets subclasses decide which class to instantiate.</p>
</li>
<li><p><strong>Abstract Factory</strong>: Provides an interface for creating families of related or dependent objects without specifying their concrete classes.</p>
</li>
<li><p><strong>Builder</strong>: Separates the construction of a complex object from its representation, so the same construction process can produce different results.</p>
</li>
<li><p><strong>Prototype</strong>: Creates new objects by cloning an existing instance rather than building one from scratch.</p>
</li>
</ol>
<h3 id="heading-1-singleton-design-pattern">1. Singleton Design Pattern</h3>
<h4 id="heading-real-world-example">Real World Example:</h4>
<p>Think of the conductor of an orchestra. An orchestra has one conductor. Every musician on stage looks to that same conductor for direction: when to start, when to stop, how fast to play, and how loud to go.</p>
<p>The conductor is the single point of authority that all musicians connect to and take decisions from. You can't have two conductors standing at the front giving different instructions. That would cause chaos. No matter which musician needs guidance, they all reach the same one person.</p>
<p>That's exactly how the Singleton works in code: one instance, shared by everyone who needs it, making decisions from one place.</p>
<h4 id="heading-problems-it-solves">Problems it solves:</h4>
<ul>
<li><p>What if two musicians get different conductors giving different instructions? The performance falls apart. There must be one conductor that every musician looks to, without exception.</p>
</li>
<li><p>How does a musician find the conductor? They don't go searching. There's one well-known place everyone looks, and the same conductor is always there.</p>
</li>
<li><p>What stops someone from appointing a second conductor? The orchestra itself controls this. Once a conductor is on the podium, no second one can take it.</p>
</li>
</ul>
<p>In simple terms, there's only one instance of the class, and every part of the system that needs it gets access to that exact same instance (never a new one).</p>
<p>Here's how Wikipedia describes the Singleton pattern:</p>
<blockquote>
<p><em>"In object-oriented programming, the singleton pattern is a software design pattern that restricts the instantiation of a class to a singular instance. The pattern is useful when exactly one object is needed to coordinate actions across a system." (</em><a href="https://en.wikipedia.org/wiki/Singleton_pattern">Source</a>)</p>
</blockquote>
<h4 id="heading-programming-example">Programming Example:</h4>
<p>We'll model the analogy directly now. The <code>OrchestraConductor</code> is the Singleton: one instance, shared by all musicians, making all decisions.</p>
<pre><code class="language-csharp">public class OrchestraConductor
{
    // Step 1: Hold the one instance here
    private static OrchestraConductor _instance;

    // Step 2: Private constructor - nobody outside can do: new OrchestraConductor()
    private OrchestraConductor() { }

    // Step 3: The only way to get the conductor
    public static OrchestraConductor GetInstance()
    {
        if (_instance == null)
        {
            _instance = new OrchestraConductor();
        }

        return _instance;
    }

    // Decisions the conductor makes
    public void Start()                    =&gt; Console.WriteLine("Conductor: Begin playing.");
    public void Stop()                     =&gt; Console.WriteLine("Conductor: Stop playing.");
    public void SetTempo(string tempo)     =&gt; Console.WriteLine($"Conductor: Tempo is now {tempo}.");
}
</code></pre>
<p>Now let's see it in action:</p>
<pre><code class="language-csharp">// Violinist asks for the conductor
OrchestraConductor violinist = OrchestraConductor.GetInstance();

// Pianist asks for the conductor
OrchestraConductor pianist = OrchestraConductor.GetInstance();

// Are they talking to the same conductor?
Console.WriteLine(object.ReferenceEquals(violinist, pianist)); // True

violinist.SetTempo("Allegro");
pianist.Start();
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="language-yaml">True
Conductor: Tempo is now Allegro.
Conductor: Begin playing.
</code></pre>
<p>Both musicians got the <strong>same conductor</strong>. The constructor never ran twice. That is the Singleton pattern.</p>
<h4 id="heading-when-to-use-the-singleton-pattern">When to Use the Singleton Pattern</h4>
<p>Reach for Singleton when you need one shared resource that the whole application talks to, such as a logger, a configuration manager, or a database connection pool.</p>
<p>It's also a good idea when having more than one instance would cause incorrect behaviour or conflicting state.</p>
<p>And it's helpful when you want a global point of access to an object without passing it around everywhere.</p>
<h3 id="heading-2-the-factory-method">2. The Factory Method</h3>
<p>Think of a recruitment agency. A company calls the agency and says "we need a worker." The company doesn't go out and create the worker themselves. They just make the request.</p>
<p>The agency decides which specific person to send: a developer, a designer, or a tester, depending on what the company needs. The company doesn't know or care exactly who is coming. They just know the person will be able to do the job.</p>
<p>That's the Factory Method. Your code asks for an object. The Factory decides which specific type to create and hands it back. You work with it without needing to know exactly what it is under the hood.</p>
<h4 id="heading-problems-it-solves">Problems it solves:</h4>
<ul>
<li><p>The company shouldn't need to know who they're getting. They just need someone who can do the job. The agency handles the decision of whom to send. The company never has to worry about the details.</p>
</li>
<li><p>What if the company needs a different type of worker tomorrow? They call the same agency. The agency decides. The company's process doesn't change, only the agency's decision does.</p>
</li>
<li><p>What if a new type of worker needs to be introduced? A new specialist agency is created to handle that. Everything else stays exactly the same.</p>
</li>
</ul>
<p>In simple terms, we define an interface for creating an object, but let subclasses decide which class to instantiate. The factory method lets a class defer instantiation to subclasses.</p>
<p>Wikipedia describes it like this:</p>
<blockquote>
<p><em>"In object-oriented programming, the factory method pattern is a design pattern that uses factory methods to deal with the problem of creating objects without having to specify their exact classes. Factory methods can be specified in an interface and implemented by subclasses, or implemented in a base class and optionally overridden by subclasses." (</em><a href="https://en.wikipedia.org/wiki/Factory_method_pattern">Source</a>)</p>
</blockquote>
<h4 id="heading-programming-example">Programming Example:</h4>
<p>The agency is the factory. The worker types are the products. The company is the client.</p>
<pre><code class="language-csharp">// The worker interface - all workers can do a job
public interface IWorker
{
    void DoWork();
}
</code></pre>
<pre><code class="language-csharp">// The concrete workers
public class Developer : IWorker
{
    public void DoWork() =&gt; Console.WriteLine("Developer: Writing code.");
}

public class Designer : IWorker
{
    public void DoWork() =&gt; Console.WriteLine("Designer: Creating designs.");
}
</code></pre>
<pre><code class="language-csharp">// The base agency - declares the factory method
public abstract class RecruitmentAgency
{
    // This is the Factory Method - subclasses decide who to hire
    public abstract IWorker HireWorker();
}
</code></pre>
<pre><code class="language-csharp">// Concrete agencies - each one decides which worker to send
public class TechAgency : RecruitmentAgency
{
    public override IWorker HireWorker() =&gt; new Developer();
}

public class DesignAgency : RecruitmentAgency
{
    public override IWorker HireWorker() =&gt; new Designer();
}
</code></pre>
<p>Now let's see it in action:</p>
<pre><code class="language-csharp">// Company A needs a tech worker
RecruitmentAgency agency = new TechAgency();
IWorker worker = agency.HireWorker();
worker.DoWork();

// Company B needs a design worker
RecruitmentAgency agency2 = new DesignAgency();
IWorker worker2 = agency2.HireWorker();
worker2.DoWork();
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="language-yaml">Developer: Writing code.
Designer: Creating designs.
</code></pre>
<p>The company never used <code>new Developer()</code> or <code>new Designer()</code> directly. The agency made that decision. That is the Factory Method.</p>
<h3 id="heading-3-the-abstract-factory-design-pattern">3. The Abstract Factory Design Pattern</h3>
<h4 id="heading-real-world-example">Real World Example</h4>
<p>Think of a furniture store that sells collections. You walk in and choose a style: Modern or Victorian. Once you choose, everything you get comes from that same collection. The sofa, the chair, and the coffee table all match. The store ensures that you never walk out with a modern sofa paired with a Victorian chair. You don't pick individual pieces and hope they go together. The collection guarantees they will.</p>
<p>That's the Abstract Factory. You choose a family, and the Factory produces every object you need from that same family. Everything it gives you is guaranteed to work together.</p>
<h4 id="heading-problems-it-solves">Problems it solves:</h4>
<ul>
<li><p>What if a customer mixes furniture from different collections? The room looks inconsistent. The store solves this by grouping everything into collections. You pick one collection and everything comes from it.</p>
</li>
<li><p>What if the store wants to introduce a new collection? They create a new collection set. Every existing collection stays untouched. The customer's experience doesn't change, only the options grow.</p>
</li>
<li><p>What if different stores carry different collections? Each store is its own factory. A customer walks into any store and follows the same process. The store handles which specific pieces to provide.</p>
</li>
</ul>
<p>In simple terms, you provide an interface for creating families of related objects, without specifying their concrete classes.</p>
<p>Wikipedia describes it like this:</p>
<blockquote>
<p><em>"The abstract factory pattern provides a way to create families of related objects without imposing their concrete classes, by encapsulating a group of individual factories that have a common theme without specifying their concrete classes."</em></p>
<p><strong>Source:</strong> <a href="https://en.wikipedia.org/wiki/Abstract_factory_pattern">Wikipedia - Abstract factory pattern</a></p>
</blockquote>
<h4 id="heading-programming-example">Programming Example:</h4>
<p>The furniture store is the abstract factory. Modern and Victorian are the concrete factories. Sofa and Chair are the products.</p>
<pre><code class="language-csharp">// The product interfaces - every furniture type has a contract
public interface ISofa  { void Describe(); }
public interface IChair { void Describe(); }
</code></pre>
<pre><code class="language-csharp">// Modern collection
public class ModernSofa : ISofa
{
    public void Describe() =&gt; Console.WriteLine("Sofa: Sleek modern design.");
}

public class ModernChair : IChair
{
    public void Describe() =&gt; Console.WriteLine("Chair: Minimalist modern style.");
}
</code></pre>
<pre><code class="language-csharp">// Victorian collection
public class VictorianSofa : ISofa
{
    public void Describe() =&gt; Console.WriteLine("Sofa: Ornate Victorian design.");
}

public class VictorianChair : IChair
{
    public void Describe() =&gt; Console.WriteLine("Chair: Classic Victorian style.");
}
</code></pre>
<pre><code class="language-csharp">// The abstract factory - every store can produce a sofa and a chair
public interface IFurnitureFactory
{
    ISofa  CreateSofa();
    IChair CreateChair();
}
</code></pre>
<pre><code class="language-csharp">// Concrete factories - each one produces its own collection
public class ModernFurnitureFactory : IFurnitureFactory
{
    public ISofa  CreateSofa()  =&gt; new ModernSofa();
    public IChair CreateChair() =&gt; new ModernChair();
}

public class VictorianFurnitureFactory : IFurnitureFactory
{
    public ISofa  CreateSofa()  =&gt; new VictorianSofa();
    public IChair CreateChair() =&gt; new VictorianChair();
}
</code></pre>
<p>Now let's see it in action:</p>
<pre><code class="language-csharp">// Customer orders a Modern collection
IFurnitureFactory factory = new ModernFurnitureFactory();
ISofa  sofa  = factory.CreateSofa();
IChair chair = factory.CreateChair();
sofa.Describe();
chair.Describe();

// Customer orders a Victorian collection
IFurnitureFactory factory2 = new VictorianFurnitureFactory();
ISofa  sofa2  = factory2.CreateSofa();
IChair chair2 = factory2.CreateChair();
sofa2.Describe();
chair2.Describe();
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="language-yaml">Sofa: Sleek modern design.
Chair: Minimalist modern style.
Sofa: Ornate Victorian design.
Chair: Classic Victorian style.
</code></pre>
<p>Every piece came from the same collection. The client never used <code>new ModernSofa()</code> or <code>new VictorianChair()</code> directly. The factory kept the family together. That's the Abstract Factory.</p>
<h4 id="heading-when-to-use-abstract-factory">When to Use Abstract Factory:</h4>
<p>Use the Abstract Factory pattern when your system needs to work with multiple families of related objects and you need to ensure they're always used together.</p>
<p>It also works well when you want to swap out an entire family of objects in one place without touching the rest of your code.</p>
<p>And it's a good choice when you want to enforce consistency across related objects, so nothing from one family gets accidentally mixed with another.</p>
<h3 id="heading-4-the-builder-design-pattern">4. The Builder Design Pattern</h3>
<h4 id="heading-real-world-example">Real World Example</h4>
<p>Think of a tailor making a suit. Every customer that walks in goes through the same process: take measurements, choose the fabric, select the lining, pick the buttons, and decide on the lapel style.</p>
<p>The tailor follows those same steps for every order. But the finished suit is completely unique to each customer. A businessman walks out with a sharp formal suit. A wedding guest walks out with something entirely different. Same process, same tailor, but with different result every time.</p>
<p>That's the Builder. The construction process stays the same. What changes are the choices made at each step.</p>
<h4 id="heading-problems-it-solves">Problems it solves:</h4>
<ul>
<li><p>What if a suit had to be assembled all at once with no steps? You would have to know every detail upfront and get it all right in one go. The tailor breaks it down into steps so each decision is made clearly, one at a time.</p>
</li>
<li><p>What if two customers want completely different suits but go through the same tailor? The tailor follows the same process for both. The steps don't change, only the choices within each step.</p>
</li>
<li><p>What if a new suit style needs to be introduced? A new set of choices is defined for that style. The tailoring process itself stays untouched.</p>
</li>
</ul>
<p>In simple terms, you separate the construction of a complex object from its representation, so that the same construction process can create different results.</p>
<p>Wikipedia describes it like this:</p>
<blockquote>
<p><em>"The Builder pattern separates the construction of a complex object from its representation so that the same construction process can create different representations."</em> <a href="https://en.wikipedia.org/wiki/Builder_pattern">(Source)</a></p>
</blockquote>
<h4 id="heading-programming-example">Programming Example:</h4>
<p>The tailor is the director. The suit is the product. The builder handles the step-by-step construction.</p>
<pre><code class="language-csharp">// The product
public class Suit
{
    public string Fabric  { get; set; }
    public string Lining  { get; set; }
    public string Buttons { get; set; }

    public void Describe()
    {
        Console.WriteLine($"Suit: {Fabric} fabric, {Lining} lining, {Buttons} buttons.");
    }
}
</code></pre>
<pre><code class="language-csharp">// The builder - defines the steps
public interface ISuitBuilder
{
    void SetFabric();
    void SetLining();
    void SetButtons();
    Suit GetSuit();
}
</code></pre>
<pre><code class="language-csharp">// Business suit builder
public class BusinessSuitBuilder : ISuitBuilder
{
    private Suit _suit = new Suit();

    public void SetFabric()  =&gt; _suit.Fabric  = "Dark wool";
    public void SetLining()  =&gt; _suit.Lining  = "Silk";
    public void SetButtons() =&gt; _suit.Buttons = "Black horn";
    public Suit GetSuit()    =&gt; _suit;
}

// Wedding suit builder
public class WeddingSuitBuilder : ISuitBuilder
{
    private Suit _suit = new Suit();

    public void SetFabric()  =&gt; _suit.Fabric  = "Ivory linen";
    public void SetLining()  =&gt; _suit.Lining  = "Satin";
    public void SetButtons() =&gt; _suit.Buttons = "Pearl";
    public Suit GetSuit()    =&gt; _suit;
}
</code></pre>
<pre><code class="language-csharp">// The tailor - the director who runs the process
public class Tailor
{
    public Suit MakeSuit(ISuitBuilder builder)
    {
        builder.SetFabric();
        builder.SetLining();
        builder.SetButtons();
        return builder.GetSuit();
    }
}
</code></pre>
<p>Now let's see it in action:</p>
<pre><code class="language-csharp">Tailor tailor = new Tailor();

Suit businessSuit = tailor.MakeSuit(new BusinessSuitBuilder());
businessSuit.Describe();

Suit weddingSuit = tailor.MakeSuit(new WeddingSuitBuilder());
weddingSuit.Describe();
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="language-plaintext">Suit: Dark wool fabric, Silk lining, Black horn buttons.
Suit: Ivory linen fabric, Satin lining, Pearl buttons.
</code></pre>
<p>The same tailor, following the same process, creates two completely different suits. That is the Builder.</p>
<h4 id="heading-when-to-use-the-builder-design-pattern">When to Use the Builder Design Pattern</h4>
<p>Use the Builder pattern when an object has many parts or configurations and building it all at once would be confusing.</p>
<p>It's also a good choice when you want the same construction process to produce different results depending on the choices made at each step.</p>
<p>And reach for it when you want to keep the construction logic separate from the object itself, so each can change independently.</p>
<h3 id="heading-5-the-prototype-design-pattern">5. The Prototype Design Pattern</h3>
<h4 id="heading-real-world-example">Real World Example</h4>
<p>Imagine you're building a drawing application. Users can create shapes like circles, rectangles, or triangles, each with its own colour, size, and position.</p>
<p>Now imagine the user wants ten red circles of the same size placed across the canvas. Creating each one from scratch means repeating the same setup ten times. What if the shape is complex, with many configured properties? That becomes expensive and repetitive.</p>
<p>The Prototype pattern solves this by letting you take one fully configured shape and clone it. The clone starts as an exact copy. The user then moves it, recolours it, or resizes it independently. The original shape is never touched. This also means new shape types can be added at runtime without the application needing to know about them in advance.</p>
<h4 id="heading-problems-it-solves">Problems it solves:</h4>
<ul>
<li><p>Creating a new shape from scratch every time is expensive. If a shape has many properties, setting them all up repeatedly wastes resources. Cloning an already configured object is far cheaper.</p>
</li>
<li><p>The application shouldn't need to know the exact type of shape it's copying. At runtime, shapes can be added or removed dynamically. The app just calls clone and gets back a ready object, whatever type it happens to be.</p>
</li>
<li><p>Modifying a copy should never affect the original. Each cloned shape is fully independent. Changes to the copy stay with the copy.</p>
</li>
</ul>
<p>In simple terms, you can create new objects by copying an existing one. The copy starts identical to the original and can then be changed independently.</p>
<p>Wikipedia describes it like this:</p>
<blockquote>
<p><em>"The Prototype pattern is used when the type of objects to create is determined by a prototypical instance, which is cloned to produce new objects." (</em><a href="https://en.wikipedia.org/wiki/Prototype_pattern">Source</a>)</p>
</blockquote>
<h4 id="heading-programming-example">Programming Example:</h4>
<p>Every shape knows how to clone itself. The application never calls <code>new Circle()</code> or <code>new Rectangle()</code> directly at runtime. It clones what already exists.</p>
<pre><code class="language-csharp">// The prototype interface - every shape must be able to clone itself
public abstract class Shape
{
    public string Colour { get; set; }
    public int    Size   { get; set; }

    public abstract Shape Clone();
    public abstract void  Describe();
}
</code></pre>
<pre><code class="language-csharp">// Concrete shapes
public class Circle : Shape
{
    public override Shape Clone()    =&gt; (Shape)this.MemberwiseClone();
    public override void  Describe() =&gt; Console.WriteLine($"Circle  | Colour: {Colour} | Size: {Size}");
}

public class Rectangle : Shape
{
    public override Shape Clone()    =&gt; (Shape)this.MemberwiseClone();
    public override void  Describe() =&gt; Console.WriteLine($"Rectangle | Colour: {Colour} | Size: {Size}");
}
</code></pre>
<p>Now let's see it in action:</p>
<pre><code class="language-csharp">// Create one configured circle
Circle original = new Circle { Colour = "Red", Size = 50 };

// Clone it instead of building from scratch
Shape clone1 = original.Clone();
Shape clone2 = original.Clone();

// Modify the clones independently
clone2.Colour = "Blue";

original.Describe();
clone1.Describe();
clone2.Describe();
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="language-plaintext">Circle  | Colour: Red  | Size: 50
Circle  | Colour: Red  | Size: 50
Circle  | Colour: Blue | Size: 50
</code></pre>
<p><code>clone2</code> changed to blue. The original stayed red. Each object is fully independent. That's the Prototype.</p>
<h4 id="heading-when-to-use-the-prototype-design-pattern">When to Use the Prototype Design Pattern:</h4>
<p>Use Prototype when creating a new object from scratch is expensive or complex and an existing object already has everything configured.</p>
<p>It's also helpful when the application needs to create objects at runtime without knowing their exact type in advance.</p>
<p>And it's great when you need many variations of an object and want to start from a known good state rather than rebuild every time.</p>
<h2 id="heading-structural-design-patterns">Structural Design Patterns</h2>
<p>Simply put, structural patterns are all about <strong>how classes and objects are composed to form larger structures</strong>. They use inheritance and composition to let you build flexible, efficient structures without having to rewrite everything from scratch.</p>
<p>Wikipedia describes them as:</p>
<blockquote>
<p><em>"In software engineering, structural patterns are design patterns that ease the design by identifying a simple way to realize relationships among entities." (</em><a href="https://en.wikipedia.org/wiki/Structural_pattern">Source</a>)</p>
</blockquote>
<p>Structural design patterns describe how objects and classes are combined to form <strong>larger, more complex structures</strong> while keeping those structures flexible and efficient.</p>
<p>They focus on composition over inheritance: how you connect things, not just what things are.</p>
<p>There are seven Structural design patterns:</p>
<ol>
<li><p><strong>Adapter</strong>: Converts one interface into another that a client expects, letting incompatible interfaces work together.</p>
</li>
<li><p><strong>Bridge</strong>: Decouples an abstraction from its implementation so the two can vary independently.</p>
</li>
<li><p><strong>Composite</strong>: Composes objects into tree structures to represent part-whole hierarchies, letting clients treat individual objects and compositions uniformly.</p>
</li>
<li><p><strong>Decorator</strong>: Attaches additional responsibilities to an object dynamically, as a flexible alternative to subclassing.</p>
</li>
<li><p><strong>Facade</strong>: Provides a simplified, unified interface to a complex subsystem.</p>
</li>
<li><p><strong>Flyweight</strong>: Uses sharing to efficiently support a large number of fine-grained objects.</p>
</li>
<li><p><strong>Proxy</strong>: Provides a surrogate or placeholder for another object to control access to it.</p>
</li>
</ol>
<h3 id="heading-1-the-adapter-design-pattern">1. The Adapter Design Pattern</h3>
<h4 id="heading-real-world-example">Real World Example</h4>
<p>Think of a language translator at a business meeting. A British CEO needs to address a Japanese team. The CEO speaks only English. The team speaks only Japanese. A translator sits between them, converting every English sentence into Japanese and delivering it to the team. Both sides keep speaking their own language. Neither the CEO nor the team change anything about how they communicate. The translator makes them compatible.</p>
<p>That's the Adapter. The client speaks one interface, while the other side speaks a different one. The Adapter sits between them and makes both sides work together without either having to change.</p>
<h4 id="heading-problems-it-solves">Problems it solves:</h4>
<ul>
<li><p>The CEO can't speak Japanese, and the team can't speak English. They're incompatible. The translator adapts one to the other without changing either side.</p>
</li>
<li><p>What if the CEO now needs to address a French team? A French translator is brought in. The CEO's process doesn't change. Only the translator changes.</p>
</li>
<li><p>What if an existing class has a useful method but the wrong interface? You wrap it in an adapter. The rest of the system talks to the adapter while the existing class stays untouched.</p>
</li>
</ul>
<p>In simple terms, you wrap an existing class with a new interface so the client can use it without any changes to either side.</p>
<p>Wikipedia describes it like this:</p>
<blockquote>
<p><em>"In software engineering, the adapter pattern is a software design pattern (also known as wrapper) that allows the interface of an existing class to be used as another interface. It is often used to make existing classes work with others without modifying their source code."</em> <a href="https://en.wikipedia.org/wiki/Adapter_pattern">(Source</a>)</p>
</blockquote>
<h4 id="heading-programming-example">Programming Example:</h4>
<p>The CEO is the client and the Japanese team member is the adaptee. They're useful, but they speak the wrong interface. The Translator is the adapter.</p>
<pre><code class="language-csharp">// What the CEO expects, someone who can receive a message in English
public interface IEnglishSpeaker
{
    void Speak(string message);
}
</code></pre>
<pre><code class="language-csharp">// The Japanese team member, speaks only Japanese (the adaptee)
public class JapaneseTeamMember
{
    public void SpeakJapanese(string message)
    {
        Console.WriteLine($"Team member (Japanese): {message}");
    }
}
</code></pre>
<pre><code class="language-csharp">// The Translator, adapts the Japanese speaker to the English interface
public class Translator : IEnglishSpeaker
{
    private readonly JapaneseTeamMember _teamMember;

    public Translator(JapaneseTeamMember teamMember)
    {
        _teamMember = teamMember;
    }

    public void Speak(string message)
    {
        string translated = TranslateToJapanese(message);
        _teamMember.SpeakJapanese(translated);
    }

    private string TranslateToJapanese(string english) =&gt; english switch
    {
        "Good morning, team."         =&gt; "おはようございます、チームの皆さん。",
        "Please review the proposal." =&gt; "提案書を確認してください。",
        _                             =&gt; $"[Japanese: {english}]"
    };
}
</code></pre>
<pre><code class="language-csharp">// The CEO, only knows how to talk to an IEnglishSpeaker
public class CEO
{
    private readonly IEnglishSpeaker _speaker;

    public CEO(IEnglishSpeaker speaker)
    {
        _speaker = speaker;
    }

    public void Address(string message)
    {
        Console.WriteLine($"CEO (English): {message}");
        _speaker.Speak(message);
    }
}
</code></pre>
<p>Now let's see it in action:</p>
<pre><code class="language-csharp">JapaneseTeamMember teamMember = new JapaneseTeamMember();
IEnglishSpeaker translator    = new Translator(teamMember);
CEO ceo = new CEO(translator);

ceo.Address("Good morning, team.");
ceo.Address("Please review the proposal.");
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="language-yaml">CEO (English): Good morning, team.
Team member (Japanese): おはようございます、チームの皆さん。
CEO (English): Please review the proposal.
Team member (Japanese): 提案書を確認してください。
</code></pre>
<p>The CEO never knew about <code>JapaneseTeamMember</code>. The team never knew about the CEO's interface. The <code>Translator</code> made both sides work together without touching either. That's the Adapter.</p>
<h4 id="heading-when-to-use-it">When to Use it</h4>
<p>Use Adapter when you want to use an existing class but its interface doesn't match what your code expects.</p>
<p>It's also helpful when you want to create a reusable class that cooperates with classes that don't have compatible interfaces.</p>
<p>And reach for it when you need to integrate a third-party library or legacy code without modifying it.</p>
<h3 id="heading-2-the-bridge-design-pattern">2. The Bridge Design Pattern</h3>
<h4 id="heading-real-world-example">Real World Example</h4>
<p>Think of a TV remote control and a television. The remote is one thing and the TV is another. You can have a basic remote or a smart remote. You can have a Sony TV or a Samsung TV. Any remote works with any TV you're not locked in. Buy a new Samsung TV, and your old remote still works. Buy a smart universal remote, and it works with every TV you own. Neither side needs to know the inner details of the other.</p>
<p>That's the Bridge. The abstraction (remote) and the implementation (TV) are two separate hierarchies that can grow and change completely independently of each other.</p>
<h4 id="heading-problems-it-solves">Problems it solves:</h4>
<ul>
<li><p>What if every remote was hardwired to one specific TV brand? You would need a SonyBasicRemote, a SamsungBasicRemote, a SonySmartRemote, a SamsungSmartRemote...and so on. One class for every combination. Adding one new TV brand would double your remote classes. The Bridge stops this explosion.</p>
</li>
<li><p>What if you want to add a new remote type without touching the TVs? With Bridge, you just create a new remote class. The TVs are untouched.</p>
</li>
<li><p>What if you want to add a new TV brand without touching the remotes? Same answer. You add a new TV class. Every existing remote already works with it.</p>
</li>
</ul>
<p>In simple terms, you split a large class into two separate hierarchies (the abstraction and the implementation) so each can be changed and extended without affecting the other.</p>
<p>Wikipedia describes it like this:</p>
<blockquote>
<p><em>"The bridge pattern is a design pattern used in software engineering that is meant to decouple an abstraction from its implementation so that the two can vary independently." (</em><a href="https://en.wikipedia.org/wiki/Bridge_pattern">Source</a>)</p>
</blockquote>
<h4 id="heading-programming-example">Programming Example:</h4>
<p>The remote control is the abstraction and the TV brand is the implementation. They're connected through a bridge (the <code>ITV</code> interface), but neither hierarchy depends on the other's details.</p>
<pre><code class="language-csharp">// The implementation interface — what any TV must be able to do
public interface ITV
{
    void TurnOn();
    void TurnOff();
    void SetChannel(int channel);
    void SetVolume(int volume);
}
</code></pre>
<pre><code class="language-csharp">// Concrete implementations — each brand handles things its own way
public class SonyTV : ITV
{
    public void TurnOn()           =&gt; Console.WriteLine("Sony TV: Powering on. BRAVIA display ready.");
    public void TurnOff()          =&gt; Console.WriteLine("Sony TV: Shutting down.");
    public void SetChannel(int ch) =&gt; Console.WriteLine($"Sony TV: Switching to channel {ch}.");
    public void SetVolume(int vol) =&gt; Console.WriteLine($"Sony TV: Volume set to {vol}.");
}

public class SamsungTV : ITV
{
    public void TurnOn()           =&gt; Console.WriteLine("Samsung TV: Turning on. Smart Hub loading.");
    public void TurnOff()          =&gt; Console.WriteLine("Samsung TV: Powering off.");
    public void SetChannel(int ch) =&gt; Console.WriteLine($"Samsung TV: Channel {ch} selected.");
    public void SetVolume(int vol) =&gt; Console.WriteLine($"Samsung TV: Volume at {vol}.");
}
</code></pre>
<pre><code class="language-csharp">// The abstraction — the remote holds a reference to whichever TV it controls
public abstract class RemoteControl
{
    protected ITV _tv;

    protected RemoteControl(ITV tv) { _tv = tv; }

    public abstract void TurnOn();
    public abstract void TurnOff();
    public abstract void SetChannel(int channel);
}
</code></pre>
<pre><code class="language-csharp">// Refined abstraction — a basic remote, does exactly what the TV does
public class BasicRemote : RemoteControl
{
    public BasicRemote(ITV tv) : base(tv) { }

    public override void TurnOn()           =&gt; _tv.TurnOn();
    public override void TurnOff()          =&gt; _tv.TurnOff();
    public override void SetChannel(int ch) =&gt; _tv.SetChannel(ch);
}

// Refined abstraction — a smart remote, adds its own behaviour on top
public class SmartRemote : RemoteControl
{
    public SmartRemote(ITV tv) : base(tv) { }

    public override void TurnOn()
    {
        Console.WriteLine("Smart Remote: Activating voice control.");
        _tv.TurnOn();
    }

    public override void TurnOff()
    {
        Console.WriteLine("Smart Remote: Saving watch history.");
        _tv.TurnOff();
    }

    public override void SetChannel(int ch)
    {
        Console.WriteLine("Smart Remote: Looking up channel guide.");
        _tv.SetChannel(ch);
    }

    public void SetVolume(int vol) =&gt; _tv.SetVolume(vol);
}
</code></pre>
<p>Now let's see it in action:</p>
<pre><code class="language-csharp">// Basic remote paired with a Sony TV
Console.WriteLine("--- Basic Remote + Sony TV ---");
RemoteControl basicSony = new BasicRemote(new SonyTV());
basicSony.TurnOn();
basicSony.SetChannel(5);
basicSony.TurnOff();

// Smart remote paired with a Samsung TV
Console.WriteLine("\n--- Smart Remote + Samsung TV ---");
SmartRemote smartSamsung = new SmartRemote(new SamsungTV());
smartSamsung.TurnOn();
smartSamsung.SetChannel(10);
smartSamsung.SetVolume(20);
smartSamsung.TurnOff();

// Swap freely — smart remote now with Sony, no code changes needed
Console.WriteLine("\n--- Smart Remote + Sony TV ---");
SmartRemote smartSony = new SmartRemote(new SonyTV());
smartSony.TurnOn();
smartSony.SetChannel(3);
smartSony.TurnOff();
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="language-yaml">--- Basic Remote + Sony TV ---
Sony TV: Powering on. BRAVIA display ready.
Sony TV: Switching to channel 5.
Sony TV: Shutting down.

--- Smart Remote + Samsung TV ---
Smart Remote: Activating voice control.
Samsung TV: Turning on. Smart Hub loading.
Smart Remote: Looking up channel guide.
Samsung TV: Channel 10 selected.
Samsung TV: Volume at 20.
Smart Remote: Saving watch history.
Samsung TV: Powering off.

--- Smart Remote + Sony TV ---
Smart Remote: Activating voice control.
Sony TV: Powering on. BRAVIA display ready.
Smart Remote: Looking up channel guide.
Sony TV: Switching to channel 3.
Smart Remote: Saving watch history.
Sony TV: Shutting down.
</code></pre>
<p>The same <code>SmartRemote</code> worked with both Sony and Samsung without any changes. Adding a new TV brand like LG means creating one new class, and every existing remote works with it immediately. That's the Bridge.</p>
<h4 id="heading-when-to-use-it">When to Use it</h4>
<p>Use the Bridge pattern when you want to avoid a permanent binding between an abstraction and its implementation, so either can be swapped at runtime.</p>
<p>You can also use it when both the abstraction and the implementation should be independently extensible through subclassing.</p>
<p>And it's a good fit when changes to the implementation should have no impact on the client code. The client shouldn't need to be recompiled.</p>
<h3 id="heading-3-the-composite-design-pattern">3. The Composite Design Pattern</h3>
<h4 id="heading-real-world-example">Real World Example</h4>
<p>Think of a company organisation chart. A company has a CEO. Under the CEO are department heads, each leading a department full of employees. Under some departments are even smaller sub-teams.</p>
<p>Now imagine you want to know the total salary cost. You can ask a single employee they tell you their salary. You can ask a whole department, which adds up every person inside it, including nested teams. Or you can ask the entire company: it rolls up every salary across every level.</p>
<p>The same question, asked the same way, whether you're talking to one person or thousands.</p>
<p>That's the Composite pattern. Individual items and groups of items share the same interface. The caller never needs to know which one they're dealing with.</p>
<h4 id="heading-problems-it-solves">Problems it solves:</h4>
<ul>
<li><p>What if you had to write different code to handle a single employee versus a whole department? You would end up with <code>if</code> checks everywhere just to figure out what you're talking to. Composite removes that entirely: one interface, always.</p>
</li>
<li><p>What if departments can contain other departments? Composite handles any depth of nesting naturally. The caller just asks the top of the tree and the operation flows down automatically.</p>
</li>
<li><p>What if you want to add a new type of team or role? You implement the same interface. Everything above it in the tree keeps working without any changes.</p>
</li>
</ul>
<p>In simple terms, you compose objects into tree structures. This lets individual objects and groups of objects be treated through the same interface, so the caller never has to care about the difference.</p>
<p>Wikipedia describes it like this:</p>
<blockquote>
<p><em>"The composite pattern describes a group of objects that are treated the same way as a single instance of the same type of object. The intent of a composite is to compose objects into tree structures to represent part-whole hierarchies."</em> <a href="https://en.wikipedia.org/wiki/Composite_pattern">(Source</a>)</p>
</blockquote>
<h4 id="heading-programming-example">Programming Example:</h4>
<p>Every node in the tree (whether a single employee or an entire department) implements <code>IEmployee</code>. The caller treats them identically.</p>
<pre><code class="language-csharp">// The component interface — every leaf and composite shares this contract
public interface IEmployee
{
    string Name    { get; }
    int    GetSalary();
    void   GetDetails(string indent = "");
}
</code></pre>
<pre><code class="language-csharp">// The leaf — a single employee with no reports
public class Employee : IEmployee
{
    private readonly int _salary;

    public string Name { get; }

    public Employee(string name, int salary)
    {
        Name    = name;
        _salary = salary;
    }

    public int  GetSalary()                    =&gt; _salary;
    public void GetDetails(string indent = "") =&gt; Console.WriteLine($"{indent}- {Name} (£{_salary:N0})");
}
</code></pre>
<pre><code class="language-csharp">// The composite — a department that holds employees or other departments
public class Department : IEmployee
{
    private readonly List&lt;IEmployee&gt; _members = new();

    public string Name { get; }

    public Department(string name) { Name = name; }

    public void Add(IEmployee employee)    =&gt; _members.Add(employee);
    public void Remove(IEmployee employee) =&gt; _members.Remove(employee);

    public int GetSalary() =&gt; _members.Sum(m =&gt; m.GetSalary());

    public void GetDetails(string indent = "")
    {
        Console.WriteLine($"{indent}[{Name}] Total: £{GetSalary():N0}");
        foreach (var member in _members)
            member.GetDetails(indent + "  ");
    }
}
</code></pre>
<p>Now let's see it in action:</p>
<pre><code class="language-csharp">// Individual employees
var ceo        = new Employee("Alice (CEO)",        120_000);
var cto        = new Employee("Bob (CTO)",           95_000);
var dev1       = new Employee("Carol (Developer)",   65_000);
var dev2       = new Employee("David (Developer)",   62_000);
var cfo        = new Employee("Eve (CFO)",           90_000);
var accountant = new Employee("Frank (Accountant)",  55_000);

// Build the Engineering department
var engineering = new Department("Engineering");
engineering.Add(cto);
engineering.Add(dev1);
engineering.Add(dev2);

// Build the Finance department
var finance = new Department("Finance");
finance.Add(cfo);
finance.Add(accountant);

// Build the whole company
var company = new Department("Acme Corp");
company.Add(ceo);
company.Add(engineering);
company.Add(finance);

// Ask the whole company — one call, rolls up everything
Console.WriteLine("=== Full Company ===");
company.GetDetails();

// Ask just one department — same call, same interface
Console.WriteLine("\n=== Engineering Only ===");
engineering.GetDetails();

// Ask a single employee — same call, same interface
Console.WriteLine("\n=== Single Employee ===");
dev1.GetDetails();
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="language-yaml">=== Full Company ===
[Acme Corp] Total: £487,000
  - Alice (CEO) (£120,000)
  [Engineering] Total: £222,000
    - Bob (CTO) (£95,000)
    - Carol (Developer) (£65,000)
    - David (Developer) (£62,000)
  [Finance] Total: £145,000
    - Eve (CFO) (£90,000)
    - Frank (Accountant) (£55,000)

=== Engineering Only ===
[Engineering] Total: £222,000
  - Bob (CTO) (£95,000)
  - Carol (Developer) (£65,000)
  - David (Developer) (£62,000)

=== Single Employee ===
- Carol (Developer) (£65,000)
</code></pre>
<p><code>company.GetDetails()</code>, <code>engineering.GetDetails()</code>, and <code>dev1.GetDetails()</code>: the same call on three different levels of the tree. The caller never checked what it was talking to. That's the Composite pattern.</p>
<h4 id="heading-when-to-use-it">When to Use it</h4>
<p>Use Composite when you need to represent part-whole hierarchies, like trees where individual items and groups of items need to be used interchangeably.</p>
<p>It also works well when you want client code to treat single objects and collections of objects uniformly, without any special-casing.</p>
<p>And it's a good fit when the structure can be nested to any depth and that depth shouldn't affect how the caller interacts with it.</p>
<h3 id="heading-4-the-decorator-design-pattern">4. The Decorator Design Pattern</h3>
<h4 id="heading-real-world-example">Real World Example</h4>
<p>Think of ordering a coffee at a cafe. You start with a plain espresso. Then you ask for milk. Then vanilla syrup. Then whipped cream on top. Each addition wraps around or adds to what was already there, adding its own cost and its own description. The espresso at the centre never changes. You're just layering on top of it, one addition at a time. You could add two shots of syrup. You could skip the milk entirely. Every combination is possible without creating a new type of coffee for each one.</p>
<p>That's the Decorator pattern. You start with a base object and wrap it in layers. Each layer adds its own behaviour and then delegates to whatever is underneath it.</p>
<h4 id="heading-problems-it-solves">Problems it solves:</h4>
<ul>
<li><p>What if you needed a class for every combination? EspressoWithMilk, EspressoWithMilkAndVanilla, EspressoWithMilkAndVanillaAndCream...the list explodes. Decorator adds behaviour at runtime, so you never need those classes.</p>
</li>
<li><p>What if the base coffee shouldn't change? It does not. The espresso class stays untouched. The decorators wrap around it and extend it independently.</p>
</li>
<li><p>What if a new topping needs to be added? You create one new decorator class. Every existing combination still works exactly as before.</p>
</li>
</ul>
<p>In simple terms, you wrap an object in one or more layers, where each layer adds its own behaviour before or after delegating to the layer beneath it.</p>
<p>Wikipedia describes it like this:</p>
<blockquote>
<p><em>"The decorator pattern is a design pattern that allows behaviour to be added to an individual object, dynamically, without affecting the behaviour of other instances of the same class."</em> <a href="https://en.wikipedia.org/wiki/Decorator_pattern">(Source</a>)</p>
</blockquote>
<h4 id="heading-programming-example">Programming Example:</h4>
<p>The coffee is the component. Each topping is a decorator. Every decorator wraps the component and adds to its description and cost.</p>
<pre><code class="language-csharp">// The component interface — every coffee, plain or decorated, shares this
public interface ICoffee
{
    string GetDescription();
    double GetCost();
}
</code></pre>
<pre><code class="language-csharp">// The base component — a plain espresso
public class Espresso : ICoffee
{
    public string GetDescription() =&gt; "Espresso";
    public double GetCost()        =&gt; 1.00;
}
</code></pre>
<pre><code class="language-csharp">// The base decorator — wraps any ICoffee and delegates to it
public abstract class CoffeeDecorator : ICoffee
{
    protected readonly ICoffee _coffee;

    protected CoffeeDecorator(ICoffee coffee) { _coffee = coffee; }

    public virtual string GetDescription() =&gt; _coffee.GetDescription();
    public virtual double GetCost()        =&gt; _coffee.GetCost();
}
</code></pre>
<pre><code class="language-csharp">// Concrete decorators — each one adds its own layer
public class Milk : CoffeeDecorator
{
    public Milk(ICoffee coffee) : base(coffee) { }

    public override string GetDescription() =&gt; _coffee.GetDescription() + ", Milk";
    public override double GetCost()        =&gt; _coffee.GetCost() + 0.30;
}

public class VanillaSyrup : CoffeeDecorator
{
    public VanillaSyrup(ICoffee coffee) : base(coffee) { }

    public override string GetDescription() =&gt; _coffee.GetDescription() + ", Vanilla Syrup";
    public override double GetCost()        =&gt; _coffee.GetCost() + 0.50;
}

public class WhippedCream : CoffeeDecorator
{
    public WhippedCream(ICoffee coffee) : base(coffee) { }

    public override string GetDescription() =&gt; _coffee.GetDescription() + ", Whipped Cream";
    public override double GetCost()        =&gt; _coffee.GetCost() + 0.75;
}
</code></pre>
<p>Now let's see it in action:</p>
<pre><code class="language-csharp">// A plain espresso
ICoffee order = new Espresso();
Console.WriteLine($"{order.GetDescription()} =&gt; £{order.GetCost():F2}");

// Wrap it with milk
order = new Milk(order);
Console.WriteLine($"{order.GetDescription()} =&gt; £{order.GetCost():F2}");

// Wrap it with vanilla syrup on top
order = new VanillaSyrup(order);
Console.WriteLine($"{order.GetDescription()} =&gt; £{order.GetCost():F2}");

// Wrap it with whipped cream on top of that
order = new WhippedCream(order);
Console.WriteLine($"{order.GetDescription()} =&gt; £{order.GetCost():F2}");
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="language-yaml">Espresso =&gt; £1.00
Espresso, Milk =&gt; £1.30
Espresso, Milk, Vanilla Syrup =&gt; £1.80
Espresso, Milk, Vanilla Syrup, Whipped Cream =&gt; £2.55
</code></pre>
<p>Each line is a new layer wrapped around the previous one. The espresso never changed. The cost and description grew with every wrapper. That's the Decorator pattern.</p>
<h4 id="heading-when-to-use-it">When to Use it</h4>
<p>Use the Decorator pattern when you want to add responsibilities to individual objects without affecting other objects of the same class.</p>
<p>It's also a good choice when subclassing would lead to an explosion of classes to cover every possible combination of behaviours.</p>
<p>And use it when you need to be able to stack behaviours in any order at runtime, independently of each other.</p>
<h3 id="heading-5-the-facade-design-pattern">5. The Facade Design Pattern</h3>
<h4 id="heading-real-world-example">Real World Example</h4>
<p>Think of clicking "Place Order" on a shopping website. In that single click, several things happen behind the scenes: the system checks whether the item is in stock, your payment is charged, a shipping label is generated, and a confirmation email is sent to you. You don't see any of that. You click one button and get one result. The complexity of four separate systems is hidden behind a single, clean action.</p>
<p>That's the Facade pattern: one simple interface in front of many complex moving parts. The caller doesn't need to know what's happening behind the scenes.</p>
<h4 id="heading-problems-it-solves">Problems it solves:</h4>
<ul>
<li><p>What if the client had to call each subsystem directly? Check inventory, then process payment, then generate a label, and then send an email, all in the right order, handling each failure separately. The Facade wraps all of that into one call.</p>
</li>
<li><p>What if one of the subsystems changes? The Facade absorbs the change. The client code never needs to know. Only the Facade is updated.</p>
</li>
<li><p>What if different clients need the same flow? They all call the same Facade method. The logic is in one place, not duplicated across every caller.</p>
</li>
</ul>
<p>In simple terms, you provide a single, simple interface that hides the complexity of a set of subsystems behind it.</p>
<p>Wikipedia describes it like this:</p>
<blockquote>
<p><em>"The facade pattern (also spelled façade) is a software-design pattern commonly used in object-oriented programming. Analogous to a facade in architecture, a facade is an object that serves as a front-facing interface masking more complex underlying or structural code."</em><a href="https://en.wikipedia.org/wiki/Facade_pattern">(Source</a>)</p>
</blockquote>
<h4 id="heading-programming-example">Programming Example:</h4>
<p>Each subsystem does its own job. The <code>OrderFacade</code> is the single entry point that coordinates all of them. The client only ever talks to the facade.</p>
<pre><code class="language-csharp">// Subsystem 1: checks whether the item is available
public class InventoryService
{
    public bool CheckStock(string item)
    {
        Console.WriteLine($"Inventory: Checking stock for {item}.");
        return true;
    }
}
</code></pre>
<pre><code class="language-csharp">// Subsystem 2: handles the payment
public class PaymentService
{
    public bool ProcessPayment(string cardNumber, double amount)
    {
        Console.WriteLine($"Payment: Charging £{amount:F2} to card ending {cardNumber[^4..]}.");
        return true;
    }
}
</code></pre>
<pre><code class="language-csharp">// Subsystem 3: generates a shipping label
public class ShippingService
{
    public string GenerateLabel(string item, string address)
    {
        Console.WriteLine($"Shipping: Generating label for {item} to {address}.");
        return "TRACK-29384";
    }
}
</code></pre>
<pre><code class="language-csharp">// Subsystem 4: sends the confirmation email
public class EmailService
{
    public void SendConfirmation(string email, string trackingCode)
    {
        Console.WriteLine($"Email: Confirmation sent to {email}. Tracking code: {trackingCode}.");
    }
}
</code></pre>
<pre><code class="language-csharp">// The Facade — one method, hides all four subsystems
public class OrderFacade
{
    private readonly InventoryService _inventory = new();
    private readonly PaymentService   _payment   = new();
    private readonly ShippingService  _shipping  = new();
    private readonly EmailService     _email     = new();

    public void PlaceOrder(string item, string cardNumber, double amount, string address, string email)
    {
        Console.WriteLine("=== Placing Order ===");

        if (!_inventory.CheckStock(item))
        {
            Console.WriteLine("Order failed: item out of stock.");
            return;
        }

        if (!_payment.ProcessPayment(cardNumber, amount))
        {
            Console.WriteLine("Order failed: payment declined.");
            return;
        }

        string trackingCode = _shipping.GenerateLabel(item, address);
        _email.SendConfirmation(email, trackingCode);

        Console.WriteLine($"\nOrder complete. Your tracking code is {trackingCode}.");
    }
}
</code></pre>
<p>Now let's see it in action:</p>
<pre><code class="language-csharp">OrderFacade store = new OrderFacade();

store.PlaceOrder(
    item:       "Wireless Headphones",
    cardNumber: "4111111111111234",
    amount:     79.99,
    address:    "42 Maple Street, London",
    email:      "customer@email.com"
);
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="language-yaml">=== Placing Order ===
Inventory: Checking stock for Wireless Headphones.
Payment: Charging £79.99 to card ending 1234.
Shipping: Generating label for Wireless Headphones to 42 Maple Street, London.
Email: Confirmation sent to customer@email.com. Tracking code: TRACK-29384.

Order complete. Your tracking code is TRACK-29384.
</code></pre>
<blockquote>
<p>The client called one method. Four subsystems ran in the right order. None of that complexity was visible to the caller. That is the Facade.</p>
</blockquote>
<h4 id="heading-when-to-use-it">When to Use it</h4>
<p>Use Facade when you want to provide a simple interface to a complex subsystem so callers aren't burdened by its internals.</p>
<p>It also works well when you want to layer your system so that high-level code talks to facades, not directly to low-level subsystems.</p>
<p>And it's a good choice when you want a single entry point that coordinates a sequence of steps across multiple services.</p>
<h3 id="heading-6-the-flyweight-design-pattern">6. The Flyweight Design Pattern</h3>
<h4 id="heading-real-world-example">Real World Example</h4>
<p>Think of a game that renders a forest. The forest has ten thousand trees. Each tree has a type name, a colour, and a texture. But most of those trees are Oaks, and all Oaks look exactly the same.</p>
<p>Creating ten thousand separate objects, each storing the same name, colour, and texture, wastes enormous amounts of memory. Instead, you create one shared Oak object that holds all that data. Every Oak tree in the forest points to that same shared object and only stores its own position on the map.</p>
<p>That's the Flyweight pattern. The data that's the same across many instances is shared. The data that's unique per instance is stored separately and passed in only when needed.</p>
<h4 id="heading-problems-it-solves">Problems it solves:</h4>
<ul>
<li><p>What if you created a full object for every single tree? With ten thousand trees, you store the same name, colour, and texture ten thousand times. Flyweight stores that shared data once and reuses it everywhere.</p>
</li>
<li><p>What if a new tree type is introduced? The factory creates one new shared object for it. Every tree of that type immediately uses it without any extra memory.</p>
</li>
<li><p>What if the forest needs to render each tree at its own position? The position is unique per tree, so it's stored on the tree itself and passed to the shared object only at render time. The shared object never holds it.</p>
</li>
</ul>
<p>In simple terms, you split an object's data into what's shared across many instances and what's unique per instance. Share the common part. Pass the unique part in only when needed.</p>
<p>Wikipedia describes it like this:</p>
<blockquote>
<p><em>"A flyweight is an object that minimizes memory usage by sharing as much data as possible with other similar objects. It is a way to use objects in large numbers when a simple repeated representation would use an unacceptable amount of memory."</em> <a href="https://en.wikipedia.org/wiki/Flyweight_pattern">(Source</a>)</p>
</blockquote>
<h4 id="heading-programming-examplel">Programming ExampleL</h4>
<p><code>TreeType</code> is the flyweight: it holds shared data. <code>Tree</code> holds only the unique position and a reference to a shared <code>TreeType</code>. The factory ensures each <code>TreeType</code> is created only once.</p>
<pre><code class="language-csharp">// The flyweight — holds shared intrinsic state (same for all trees of this type)
public class TreeType
{
    public string Name    { get; }
    public string Colour  { get; }
    public string Texture { get; }

    public TreeType(string name, string colour, string texture)
    {
        Name    = name;
        Colour  = colour;
        Texture = texture;
    }

    public void Render(int x, int y)
    {
        Console.WriteLine($"Rendering {Name} tree ({Colour}, {Texture}) at ({x}, {y})");
    }
}
</code></pre>
<pre><code class="language-csharp">// The flyweight factory — creates and caches tree types so they are never duplicated
public class TreeTypeFactory
{
    private readonly Dictionary&lt;string, TreeType&gt; _cache = new();

    public TreeType GetTreeType(string name, string colour, string texture)
    {
        string key = $"{name}_{colour}_{texture}";

        if (!_cache.ContainsKey(key))
        {
            Console.WriteLine($"Factory: Creating new TreeType for '{name}'.");
            _cache[key] = new TreeType(name, colour, texture);
        }

        return _cache[key];
    }

    public int TotalTypes =&gt; _cache.Count;
}
</code></pre>
<pre><code class="language-csharp">// The context — holds unique extrinsic state (position) and a reference to a shared flyweight
public class Tree
{
    private readonly int      _x;
    private readonly int      _y;
    private readonly TreeType _type;

    public Tree(int x, int y, TreeType type)
    {
        _x    = x;
        _y    = y;
        _type = type;
    }

    public void Render() =&gt; _type.Render(_x, _y);
}
</code></pre>
<pre><code class="language-csharp">// The forest — plants trees using shared flyweights
public class Forest
{
    private readonly List&lt;Tree&gt;      _trees   = new();
    private readonly TreeTypeFactory _factory = new();

    public void PlantTree(int x, int y, string name, string colour, string texture)
    {
        TreeType type = _factory.GetTreeType(name, colour, texture);
        _trees.Add(new Tree(x, y, type));
    }

    public void Render()
    {
        foreach (var tree in _trees)
            tree.Render();
    }

    public int TreeCount     =&gt; _trees.Count;
    public int TreeTypeCount =&gt; _factory.TotalTypes;
}
</code></pre>
<p>Now let's see it in action:</p>
<pre><code class="language-csharp">Forest forest = new Forest();

// Plant 6 trees — but only 2 unique types
forest.PlantTree(1,  5,  "Oak",  "Dark Green",  "Rough bark");
forest.PlantTree(3,  12, "Oak",  "Dark Green",  "Rough bark");
forest.PlantTree(7,  2,  "Oak",  "Dark Green",  "Rough bark");
forest.PlantTree(10, 8,  "Pine", "Light Green", "Smooth bark");
forest.PlantTree(15, 3,  "Pine", "Light Green", "Smooth bark");
forest.PlantTree(20, 14, "Pine", "Light Green", "Smooth bark");

forest.Render();

Console.WriteLine($"\nTrees planted:              {forest.TreeCount}");
Console.WriteLine($"Unique tree types in memory: {forest.TreeTypeCount}");
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="language-plaintext">Factory: Creating new TreeType for 'Oak'.
Factory: Creating new TreeType for 'Pine'.
Rendering Oak tree (Dark Green, Rough bark) at (1, 5)
Rendering Oak tree (Dark Green, Rough bark) at (3, 12)
Rendering Oak tree (Dark Green, Rough bark) at (7, 2)
Rendering Pine tree (Light Green, Smooth bark) at (10, 8)
Rendering Pine tree (Light Green, Smooth bark) at (15, 3)
Rendering Pine tree (Light Green, Smooth bark) at (20, 14)

Trees planted:              6
Unique tree types in memory: 2
</code></pre>
<p>Six trees, but only two <code>TreeType</code> objects were ever created. Scale that to ten thousand trees and the factory still creates exactly two. The positions are unique per tree, and the appearance is shared. That's the Flyweight.</p>
<h4 id="heading-when-to-use-it">When to Use it</h4>
<p>Use Flyweight when your application needs to create a very large number of similar objects that would otherwise consume too much memory.</p>
<p>It's also useful when most of the object's state can be made shared across instances, with only a small part being unique per instance.</p>
<p>And it's a good choice when the unique part of the state can be passed in externally rather than stored inside every object.</p>
<h3 id="heading-7-the-proxy-design-pattern">7. The Proxy Design Pattern</h3>
<h4 id="heading-real-world-example">Real World Example</h4>
<p>Think of a security guard at the entrance of an office building. You can't walk straight into the building. You have to go through the guard first. The guard checks your name against the authorised list, logs your visit, and only then lets you through. If you're not on the list, you're turned away. The building itself never deals with any of that. It just lets people in. All the checking, logging, and decision-making happens at the guard (the proxy) before the building ever gets involved.</p>
<p>That's the Proxy pattern. It sits between the caller and the real object, controls what gets through, and can add behaviour like access checks or logging without the real object knowing anything about it.</p>
<h4 id="heading-problems-it-solves">Problems it solves:</h4>
<ul>
<li><p>What if anyone could walk straight into the building? There would be no access control. The proxy intercepts every request and decides whether it should be allowed through.</p>
</li>
<li><p>What if you need to log every entry without changing the building? The proxy handles it. The real building stays simple and focused on its own job.</p>
</li>
<li><p>What if the real object is expensive to create and you want to delay that? The proxy can hold off creating it until someone actually passes the check and needs it.</p>
</li>
</ul>
<p>In simple terms, you place an object in front of another object to control access to it. The caller thinks it's talking directly to the real object, but the proxy is handling it first.</p>
<p>Wikipedia describes it like this:</p>
<blockquote>
<p><em>"A proxy, in its most general form, is a class functioning as an interface to something else. The proxy could interface to anything: a network connection, a large object in memory, a file, or some other resource that is expensive or impossible to duplicate." (</em><a href="https://en.wikipedia.org/wiki/Proxy_pattern">Source</a>)</p>
</blockquote>
<h4 id="heading-programming-example">Programming Example:</h4>
<p>The client talks to <code>IBuilding</code>. The <code>SecurityGuard</code> is the proxy: it implements the same interface, controls access, and only lets authorised visitors through to the <code>OfficeBuilding</code>.</p>
<pre><code class="language-csharp">// The subject interface — the building and the proxy both implement this
public interface IBuilding
{
    void Enter(string visitorName);
}
</code></pre>
<pre><code class="language-csharp">// The real subject — the actual building, just grants entry
public class OfficeBuilding : IBuilding
{
    public void Enter(string visitorName)
    {
        Console.WriteLine($"Building: {visitorName} has entered.");
    }
}
</code></pre>
<pre><code class="language-csharp">// The proxy — the security guard controls who gets through
public class SecurityGuard : IBuilding
{
    private readonly OfficeBuilding _building          = new();
    private readonly List&lt;string&gt;   _authorisedVisitors = new() { "Alice", "Bob", "Carol" };

    public void Enter(string visitorName)
    {
        Console.WriteLine($"Guard: {visitorName} is requesting entry.");

        if (_authorisedVisitors.Contains(visitorName))
        {
            Console.WriteLine("Guard: ID verified. Access granted.");
            _building.Enter(visitorName);
        }
        else
        {
            Console.WriteLine($"Guard: {visitorName} is not on the list. Access denied.");
        }
    }
}
</code></pre>
<p>Now let's see it in action:</p>
<pre><code class="language-csharp">IBuilding entrance = new SecurityGuard();

entrance.Enter("Alice");
Console.WriteLine();
entrance.Enter("David");
Console.WriteLine();
entrance.Enter("Bob");
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="language-yaml">Guard: Alice is requesting entry.
Guard: ID verified. Access granted.
Building: Alice has entered.

Guard: David is requesting entry.
Guard: David is not on the list. Access denied.

Guard: Bob is requesting entry.
Guard: ID verified. Access granted.
Building: Bob has entered.
</code></pre>
<p>The client called <code>Enter()</code> on what it thought was the building. It was actually the security guard. The guard decided what happened. The building only ever saw the people who were allowed through. That's the Proxy.</p>
<h4 id="heading-when-to-use-it">When to Use it</h4>
<p>Use Proxy when you need access control, like only letting certain callers through to the real object.</p>
<p>It's a good fit when you want to add behaviour such as logging, caching, or validation without changing the real object.</p>
<p>And you can use it when the real object is expensive to create and you want to delay or guard that creation until it's truly needed.</p>
<h2 id="heading-behavioral-design-patterns">Behavioral Design Patterns</h2>
<p>Simply put, behavioral patterns are all about <strong>how objects communicate and share responsibility</strong>. They focus on the assignment of responsibilities between objects, and how objects cooperate to get a job done.</p>
<p>Wikipedia describes them as:</p>
<blockquote>
<p><em>"In software engineering, behavioral design patterns are design patterns that identify common communication patterns among objects. By doing so, these patterns increase flexibility in carrying out this communication." (</em><a href="https://en.wikipedia.org/wiki/Behavioral_pattern">Source</a>)</p>
</blockquote>
<p>Behavioral design patterns describe how objects interact and distribute responsibility, not just how they're structured. They focus on communication between objects: who talks to whom, and how much each side knows about the other.</p>
<p>There are 11 behavioral design patterns:</p>
<ol>
<li><p><strong>Chain of Responsibility</strong>: Passes a request along a chain of handlers, letting each one decide to handle it or pass it on.</p>
</li>
<li><p><strong>Command</strong>: Encapsulates a request as an object, letting you parameterize clients, queue actions, and support undo.</p>
</li>
<li><p><strong>Interpreter</strong>: Given a language, defines a representation for its grammar along with an interpreter that evaluates sentences in it.</p>
</li>
<li><p><strong>Iterator</strong>: Provides a way to access the elements of a collection sequentially without exposing how it's built underneath.</p>
</li>
<li><p><strong>Mediator</strong>: Defines an object that encapsulates how a set of objects interact, so they don't refer to each other directly.</p>
</li>
<li><p><strong>Memento</strong>: Captures an object's internal state so it can be restored later, without breaking encapsulation.</p>
</li>
<li><p><strong>Observer</strong>: Defines a one-to-many dependency so that when one object changes state, everything depending on it is notified automatically.</p>
</li>
<li><p><strong>State</strong>: Lets an object change its behaviour when its internal state changes, as if it had changed its class.</p>
</li>
<li><p><strong>Strategy</strong>: Defines a family of interchangeable algorithms and lets the client pick which one to use at runtime.</p>
</li>
<li><p><strong>Template Method</strong>: Defines the skeleton of an algorithm in a method, leaving some steps for subclasses to fill in.</p>
</li>
<li><p><strong>Visitor</strong>: Lets you define a new operation without changing the classes of the elements it operates on.</p>
</li>
</ol>
<h3 id="heading-1-the-chain-of-responsibility-design-pattern">1. The Chain of Responsibility Design Pattern</h3>
<h4 id="heading-real-world-example">Real World Example</h4>
<p>Think of an expense approval process at a company. An employee submits a request to their director. If the amount is small enough, the director approves it and that's the end of it. If it's too large for the director to sign off on, it goes up to the vice president. If it's still too large, it goes up to the chief executive.</p>
<p>Each person in the chain only needs to know two things: what they're allowed to approve, and who to hand it to if they can't. The employee never needs to know who ends up approving it.</p>
<p>That's the Chain of Responsibility design pattern. A request travels along a chain of handlers until one of them deals with it, and each handler only cares about its own link in that chain.</p>
<h4 id="heading-problems-it-solves">Problems it solves:</h4>
<ul>
<li><p>What if the sender had to know exactly who should handle the request? That would tie the sender to a specific handler and break the moment the approval structure changed. The chain lets the sender submit the request without knowing who will end up handling it.</p>
</li>
<li><p>What if one handler could only ever approve or reject, with no fallback? Requests that fell outside its authority would simply fail. The chain lets a handler pass what it can't deal with further along.</p>
</li>
<li><p>What if you needed to change the approval structure? Rewiring which handler comes after which is enough. Neither the sender nor the other handlers need to change.</p>
</li>
</ul>
<p>In simple terms, you pass a request along a chain of handlers. Each handler decides whether to deal with it or hand it off to the next one in line.</p>
<p>Wikipedia describes it like this:</p>
<blockquote>
<p><em>"In object-oriented design, the chain-of-responsibility pattern is a behavioral design pattern consisting of a source of command objects and a series of processing objects. Each processing object contains logic that defines the types of command objects that it can handle; the rest are passed to the next processing object in the chain."</em></p>
<p><strong>(</strong><a href="https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern">Source</a>)</p>
</blockquote>
<h4 id="heading-programming-example">Programming Example:</h4>
<p>Each <code>Approver</code> knows its own approval limit and holds a reference to the next approver in the chain. The <code>ExpenseRequest</code> is passed along until someone can approve it, or nobody can.</p>
<pre><code class="language-csharp">// The request that travels along the chain
public class ExpenseRequest
{
    public string Description { get; }
    public decimal Amount     { get; }

    public ExpenseRequest(string description, decimal amount)
    {
        Description = description;
        Amount      = amount;
    }
}
</code></pre>
<pre><code class="language-csharp">// The handler, every link in the chain implements this
public abstract class Approver
{
    private Approver? _next;

    public void SetNext(Approver next) =&gt; _next = next;

    public void Approve(ExpenseRequest request)
    {
        if (CanApprove(request))
        {
            Console.WriteLine($"{GetType().Name}: Approved '{request.Description}' (${request.Amount}).");
        }
        else if (_next is not null)
        {
            Console.WriteLine($"{GetType().Name}: Can't approve '{request.Description}' (${request.Amount}). Passing it up.");
            _next.Approve(request);
        }
        else
        {
            Console.WriteLine($"{GetType().Name}: No one left to approve '{request.Description}' (${request.Amount}). Request denied.");
        }
    }

    protected abstract bool CanApprove(ExpenseRequest request);
}
</code></pre>
<pre><code class="language-csharp">// Concrete handlers, each with its own approval limit
public class Director : Approver
{
    protected override bool CanApprove(ExpenseRequest request) =&gt; request.Amount &lt;= 1000;
}

public class VicePresident : Approver
{
    protected override bool CanApprove(ExpenseRequest request) =&gt; request.Amount &lt;= 20000;
}

public class Chief : Approver
{
    protected override bool CanApprove(ExpenseRequest request) =&gt; request.Amount &lt;= 50000;
}
</code></pre>
<p>Now let's see it in action:</p>
<pre><code class="language-csharp">Approver directorApprover = new Director();
Approver vpApprover       = new VicePresident();
Approver ceoApprover      = new Chief();

directorApprover.SetNext(vpApprover);
vpApprover.SetNext(ceoApprover);

directorApprover.Approve(new ExpenseRequest("Laptop", 800));
Console.WriteLine();
directorApprover.Approve(new ExpenseRequest("Team offsite", 12000));
Console.WriteLine();
directorApprover.Approve(new ExpenseRequest("New office lease", 90000));
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="language-yaml">Director: Approved 'Laptop' ($800).

Director: Can't approve 'Team offsite' ($12000). Passing it up.
VicePresident: Approved 'Team offsite' ($12000).

Director: Can't approve 'New office lease' ($90000). Passing it up.
VicePresident: Can't approve 'New office lease' ($90000). Passing it up.
Chief: No one left to approve 'New office lease' ($90000). Request denied.
</code></pre>
<p>The employee only ever talked to the director. Whether the director, the vice president, or the chief ended up approving it, was decided by the chain itself, not the employee.</p>
<h4 id="heading-when-to-use-it">When to Use it</h4>
<p>Use Chain of Responsibility when more than one object might handle a request, and the handler isn't known in advance.</p>
<p>It's also a good choice when you want to issue a request without specifying the receiver explicitly.</p>
<p>And it's helpful when the set of handlers, and their order, should be configurable rather than hard-coded.</p>
<h3 id="heading-2-the-command-design-pattern">2. The Command Design Pattern</h3>
<h4 id="heading-real-world-example">Real World Example</h4>
<p>Think of a universal remote control. Every button is programmed to do one specific thing: turn a light on, turn a light off, and so on. When you press a button, the remote doesn't know or care how the light actually works internally. It just triggers the action that button was set up to perform. And because each button's action is a self-contained thing, the remote can also press it in reverse, undoing what it just did.</p>
<p>That's the Command pattern. A request "turn the light on" is wrapped up as its own object. The thing that triggers it doesn't need to know anything about how it's carried out.</p>
<h4 id="heading-problems-it-solves">Problems it solves:</h4>
<ul>
<li><p>What if the button had to know exactly how the light worked? Every button would need to be rewritten if the light's internals changed. Wrapping the action as a command means the remote never touches those details.</p>
</li>
<li><p>What if you wanted to undo the last action? Without a command object there's nothing to reverse, only a completed side effect. Wrapping the action gives you something you can also unwind.</p>
</li>
<li><p>What if you wanted to queue actions, log them, or trigger them later? A plain method call happens immediately and leaves nothing behind. A command is an object, so it can be stored, queued, and replayed.</p>
</li>
</ul>
<p>In simple terms, you wrap a request up as an object, so the thing that triggers it doesn't need to know how it's carried out, and the action itself can be queued, logged, or undone.</p>
<p>Wikipedia describes it like this:</p>
<blockquote>
<p><em>"The command pattern is a behavioral design pattern in which an object is used to encapsulate all information needed to perform an action or trigger an event at a later time." (</em><a href="https://en.wikipedia.org/wiki/Command_pattern">Source</a>)</p>
</blockquote>
<h4 id="heading-programming-example">Programming Example:</h4>
<p>The <code>RemoteControl</code> is the invoker it only knows about <code>ICommand</code>. <code>LightOnCommand</code> and <code>LightOffCommand</code> are the concrete commands, each wrapping the <code>Light</code> receiver and the action to perform on it.</p>
<pre><code class="language-csharp">// The command interface, every action implements this
public interface ICommand
{
    void Execute();
    void Undo();
}
</code></pre>
<pre><code class="language-csharp">// The receiver, the object that actually does the work
public class Light
{
    private readonly string _room;

    public Light(string room) =&gt; _room = room;

    public void On()  =&gt; Console.WriteLine($"{_room} light: turned on.");
    public void Off() =&gt; Console.WriteLine($"{_room} light: turned off.");
}
</code></pre>
<pre><code class="language-csharp">// Concrete commands, each wraps a receiver and an action
public class LightOnCommand : ICommand
{
    private readonly Light _light;

    public LightOnCommand(Light light) =&gt; _light = light;

    public void Execute() =&gt; _light.On();
    public void Undo()    =&gt; _light.Off();
}

public class LightOffCommand : ICommand
{
    private readonly Light _light;

    public LightOffCommand(Light light) =&gt; _light = light;

    public void Execute() =&gt; _light.Off();
    public void Undo()    =&gt; _light.On();
}
</code></pre>
<pre><code class="language-csharp">// The invoker, it holds a command and triggers it without knowing what it does
public class RemoteControl
{
    private ICommand? _command;

    public void SetCommand(ICommand command) =&gt; _command = command;

    public void PressButton() =&gt; _command?.Execute();
    public void PressUndo()   =&gt; _command?.Undo();
}
</code></pre>
<p>Now let's see it in action:</p>
<pre><code class="language-csharp">var livingRoomLight = new Light("Living Room");
var remote          = new RemoteControl();

remote.SetCommand(new LightOnCommand(livingRoomLight));
remote.PressButton();

remote.SetCommand(new LightOffCommand(livingRoomLight));
remote.PressButton();

Console.WriteLine();
Console.WriteLine("Undoing last action...");
remote.PressUndo();
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="language-yaml">Living Room light: turned on.
Living Room light: turned off.

Undoing last action...
Living Room light: turned on.
</code></pre>
<p>The remote never called <code>_light.On()</code> or <code>_light.Off()</code> directly. It called <code>Execute()</code> and <code>Undo()</code> on whatever command it was holding. That's the Command pattern: the request itself became an object.</p>
<h4 id="heading-when-to-use-it">When to Use it</h4>
<p>Use the Command pattern when you want to parameterize objects with an action to perform, rather than hard-coding it.</p>
<p>Reach for it when you need to queue, log, or support undo for requests.</p>
<p>And consider it when you want to decouple the object that invokes an action from the object that knows how to perform it.</p>
<h3 id="heading-3-the-interpreter-design-pattern">3. The Interpreter Design Pattern</h3>
<h4 id="heading-real-world-example">Real World Example</h4>
<p>Think of a basic calculator reading an expression like <code>(5 + 3) - 2</code>. Nobody hardcodes a single method that handles every possible expression. Instead, the expression is broken down into small pieces: numbers and operation buttons, each of which knows how to evaluate itself and combine with the others. <code>(5 + 3) - 2</code> becomes a subtraction of two things: the number 2, and the result of adding 5 and 3. Each piece only needs to know how to interpret itself.</p>
<p>That's the Interpreter pattern. A grammar is represented as a tree of small objects, and each one knows how to evaluate its own little piece of it.</p>
<h4 id="heading-problems-it-solves">Problems it solves:</h4>
<ul>
<li><p>What if you tried to evaluate an entire expression in one big method? It would grow unmanageable the moment the grammar got more complex. Breaking the grammar into small classes, one per rule, keeps each piece simple.</p>
</li>
<li><p>What if the grammar needed to grow? Adding a new operation, like multiplication, is just a new class. The existing pieces don't need to change.</p>
</li>
<li><p>What if the same expression needed to be evaluated more than once, or in different contexts? Because each piece is just an object, the same tree can be interpreted again without rebuilding it.</p>
</li>
</ul>
<p>In simple terms, you represent a grammar as a tree of small objects, where each object knows how to interpret its own piece of the expression.</p>
<p>Wikipedia describes it like this:</p>
<blockquote>
<p><em>"In computer programming, the interpreter pattern is a design pattern that specifies how to evaluate sentences in a language. The basic idea is to have a class for each symbol (terminal or nonterminal) in a specialized computer language." (</em><a href="https://en.wikipedia.org/wiki/Interpreter_pattern">Source</a>)</p>
</blockquote>
<h4 id="heading-programming-example">Programming Example:</h4>
<p><code>Number</code> is the terminal expression, a plain value. <code>Add</code> and <code>Subtract</code> are non-terminal expressions, each combining two other expressions. Every node, terminal or not, knows how to <code>Interpret()</code> itself.</p>
<pre><code class="language-csharp">// The abstract expression, every node in the grammar implements this
public abstract class Expression
{
    public abstract int Interpret();
}
</code></pre>
<pre><code class="language-csharp">// A terminal expression, a plain number that needs no further interpretation
public class Number : Expression
{
    private readonly int _value;

    public Number(int value) =&gt; _value = value;

    public override int Interpret() =&gt; _value;
}
</code></pre>
<pre><code class="language-csharp">// Non-terminal expressions, each combines other expressions
public class Add : Expression
{
    private readonly Expression _left;
    private readonly Expression _right;

    public Add(Expression left, Expression right)
    {
        _left  = left;
        _right = right;
    }

    public override int Interpret() =&gt; _left.Interpret() + _right.Interpret();
}

public class Subtract : Expression
{
    private readonly Expression _left;
    private readonly Expression _right;

    public Subtract(Expression left, Expression right)
    {
        _left  = left;
        _right = right;
    }

    public override int Interpret() =&gt; _left.Interpret() - _right.Interpret();
}
</code></pre>
<p>Now let's see it in action:</p>
<pre><code class="language-csharp">// (5 plus 3) minus 2
Expression expression = new Subtract(
    new Add(new Number(5), new Number(3)),
    new Number(2)
);

Console.WriteLine($"Result: {expression.Interpret()}");

// (10 minus 4) plus (2 plus 2)
Expression another = new Add(
    new Subtract(new Number(10), new Number(4)),
    new Add(new Number(2), new Number(2))
);

Console.WriteLine($"Result: {another.Interpret()}");
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="language-yaml">Result: 6
Result: 10
</code></pre>
<p>Nothing ever evaluated the whole expression at once. <code>Subtract</code> asked its own <code>_left</code> and <code>_right</code> to interpret themselves, and those asked their own children, all the way down to plain numbers. That's the Interpreter pattern: the grammar interprets itself, one small piece at a time.</p>
<h4 id="heading-when-to-use-it">When to Use it</h4>
<p>Use Interpreter when you have a simple language or grammar to evaluate, and representing it as a tree of expressions keeps it manageable.</p>
<p>It also works well when the grammar is relatively stable. For example, adding new rules means adding new classes, not rewriting existing ones.</p>
<p>And it's useful when you would rather have many small, focused classes than one large method trying to parse and evaluate everything at once.</p>
<h3 id="heading-4-the-iterator-design-pattern">4. The Iterator Design Pattern</h3>
<h4 id="heading-real-world-example">Real World Example</h4>
<p>Think of a bookshelf. You want to go through it one book at a time, from left to right, without needing to know whether the books are held in an array, multiple piles and stacks, or something else entirely. All you need is a way to ask "what's next?" and to know when you've reached the end. How the shelf actually stores its books internally is none of your concern.</p>
<p>That's the Iterator pattern. It gives you a consistent way to step through a collection, one element at a time, without exposing how that collection is built underneath.</p>
<h4 id="heading-problems-it-solves">Problems it solves:</h4>
<ul>
<li><p>What if the client had to know how the collection was stored internally to loop over it? Any change to that internal structure would break every piece of code that loops over it. The iterator hides that structure behind a simple "get next" interface.</p>
</li>
<li><p>What if you needed more than one traversal in progress at the same time? A single shared position wouldn't work. Each iterator keeps its own position, so multiple traversals can happen independently.</p>
</li>
<li><p>What if you wanted to loop over the collection using the language's own <code>foreach</code>? Implementing the iterator interface the language expects means your custom collection gets that support for free.</p>
</li>
</ul>
<p>In simple terms, you give a collection a way to be walked through, one element at a time, without exposing how it's actually built underneath.</p>
<p>Wikipedia describes it like this:</p>
<blockquote>
<p><em>"In object-oriented programming, the iterator pattern is a design pattern in which an iterator is used to traverse a container and access the container's elements."</em> <a href="https://en.wikipedia.org/wiki/Iterator_pattern">(Source</a>)</p>
</blockquote>
<h4 id="heading-programming-example">Programming Example:</h4>
<p><code>Bookshelf</code> is the aggregate it exposes an <code>IEnumerator&lt;string&gt;</code> without revealing that it stores books in a <code>List&lt;string&gt;</code> internally. <code>BookshelfIterator</code> is the iterator that walks through them one at a time.</p>
<pre><code class="language-csharp">// The aggregate, exposes an iterator without revealing how books are stored
public class Bookshelf : IEnumerable&lt;string&gt;
{
    private readonly List&lt;string&gt; _books = new();

    public void Add(string title) =&gt; _books.Add(title);

    public IEnumerator&lt;string&gt; GetEnumerator() =&gt; new BookshelfIterator(_books);

    IEnumerator IEnumerable.GetEnumerator() =&gt; GetEnumerator();
}
</code></pre>
<pre><code class="language-csharp">// The iterator, walks the collection one book at a time
public class BookshelfIterator : IEnumerator&lt;string&gt;
{
    private readonly List&lt;string&gt; _books;
    private int _position = -1;

    public BookshelfIterator(List&lt;string&gt; books) =&gt; _books = books;

    public string Current =&gt; _books[_position];

    object IEnumerator.Current =&gt; Current;

    public bool MoveNext()
    {
        _position++;
        return _position &lt; _books.Count;
    }

    public void Reset() =&gt; _position = -1;

    public void Dispose() { }
}
</code></pre>
<p>Now let's see it in action:</p>
<pre><code class="language-csharp">var bookshelf = new Bookshelf();
bookshelf.Add("Clean Code");
bookshelf.Add("The Pragmatic Programmer");
bookshelf.Add("Design Patterns");

foreach (var book in bookshelf)
{
    Console.WriteLine($"On the shelf: {book}");
}
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="language-yaml">On the shelf: Clean Code
On the shelf: The Pragmatic Programmer
On the shelf: Design Patterns
</code></pre>
<p>The <code>foreach</code> loop never touched the <code>List&lt;string&gt;</code> inside <code>Bookshelf</code> directly. It called <code>MoveNext()</code> and <code>Current</code> on the <code>BookshelfIterator</code>, one step at a time. That's the Iterator pattern: the traversal logic lives outside the collection itself.</p>
<h4 id="heading-when-to-use-it">When to Use it</h4>
<p>Use Iterator when you want to traverse a collection without exposing its internal structure.</p>
<p>It's also a good choice when you need to support multiple simultaneous traversals over the same collection.</p>
<p>And try it when you want your custom collection to work with the language's built-in iteration syntax, like <code>foreach</code>.</p>
<h3 id="heading-5-the-mediator-design-pattern">5. The Mediator Design Pattern</h3>
<h4 id="heading-real-world-example">Real World Example</h4>
<p>Think of an air traffic control tower. Planes don't radio each other directly to negotiate who lands first. That would be chaos: dozens of pilots all trying to coordinate with each other at once.</p>
<p>Instead, every plane talks only to the tower. The tower knows the state of the runway and tells each plane what to do. The planes never need to know how many other planes are around, or what they're doing.</p>
<p>That's the Mediator pattern. Instead of objects talking to each other directly, they all talk to one central object that coordinates them.</p>
<h4 id="heading-problems-it-solves">Problems it solves:</h4>
<ul>
<li><p>What if every aircraft had to communicate directly with every other aircraft? The number of connections would explode as more aircraft joined, and each one would need to know about all the others. The mediator means each aircraft only needs to know about the tower.</p>
</li>
<li><p>What if the coordination logic was scattered across every object involved? Changing how landings get prioritised would mean touching every aircraft. With a mediator, that logic lives in one place.</p>
</li>
<li><p>What if you wanted to add a new aircraft to the system? It only needs to know how to talk to the tower. It doesn't need to be introduced to every other aircraft already in the sky.</p>
</li>
</ul>
<p>In simple terms, instead of letting objects talk to each other directly, you route all communication through one central object that knows how to coordinate them.</p>
<p>Wikipedia describes it like this:</p>
<blockquote>
<p><em>"In software engineering, the mediator pattern defines an object that encapsulates how a set of objects interact. This pattern is considered to be a behavioral pattern due to the way it can alter the program's running behavior." (</em><a href="https://en.wikipedia.org/wiki/Mediator_pattern">Source</a>)</p>
</blockquote>
<h4 id="heading-programming-example">Programming Example:</h4>
<p><code>ControlTower</code> is the mediator. It's the only thing an <code>Aircraft</code> ever talks to. It decides whether a plane can land based on the state it holds, and no aircraft ever contacts another aircraft directly.</p>
<pre><code class="language-csharp">// The mediator interface
public interface IControlTower
{
    void RequestLanding(Aircraft requester);
}
</code></pre>
<pre><code class="language-csharp">// The concrete mediator, coordinates all the aircraft instead of letting them talk to each other
public class ControlTower : IControlTower
{
    private readonly List&lt;Aircraft&gt; _aircraft = new();
    private bool _runwayFree = true;

    public void Register(Aircraft aircraft) =&gt; _aircraft.Add(aircraft);

    public void RequestLanding(Aircraft requester)
    {
        if (_runwayFree)
        {
            _runwayFree = false;
            Console.WriteLine($"Tower: Runway clear. {requester.Name}, you are cleared to land.");
        }
        else
        {
            Console.WriteLine($"Tower: Runway occupied. {requester.Name}, please hold your position.");
        }
    }
}
</code></pre>
<pre><code class="language-csharp">// The colleague, only ever talks to the mediator, never to other aircraft directly
public class Aircraft
{
    public string Name { get; }

    private readonly IControlTower _tower;

    public Aircraft(string name, IControlTower tower)
    {
        Name   = name;
        _tower = tower;
    }

    public void RequestLanding()
    {
        Console.WriteLine($"{Name}: Requesting permission to land.");
        _tower.RequestLanding(this);
    }
}
</code></pre>
<p>Now let's see it in action:</p>
<pre><code class="language-csharp">var tower = new ControlTower();

var flight101 = new Aircraft("Flight 101", tower);
var flight202 = new Aircraft("Flight 202", tower);

tower.Register(flight101);
tower.Register(flight202);

flight101.RequestLanding();
flight202.RequestLanding();
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="language-yaml">Flight 101: Requesting permission to land.
Tower: Runway clear. Flight 101, you are cleared to land.
Flight 202: Requesting permission to land.
Tower: Runway occupied. Flight 202, please hold your position.
</code></pre>
<p>Flight 101 and Flight 202 never spoke to each other. Neither one even knows the other exists. Both only ever talked to the tower, and the tower decided what happened next. That's the Mediator pattern.</p>
<h4 id="heading-when-to-use-it">When to Use it</h4>
<p>Use Mediator when a group of objects communicate in complex, tangled ways, and you want to centralise that communication.</p>
<p>It's also helpful when you want to reuse objects independently, without them being locked together by direct references to each other.</p>
<p>And reach for it when the way objects interact changes often, and you'd rather change it in one place than in every object involved.</p>
<h3 id="heading-6-the-memento-design-pattern">6. The Memento Design Pattern</h3>
<h4 id="heading-real-world-example">Real World Example</h4>
<p>Think of the undo history in a text editor. Every so often, the editor quietly takes a snapshot of what the document looks like. It doesn't ask the document to expose its internals to do this. It just captures a copy of the content at that moment.</p>
<p>When you press undo, the editor hands that snapshot back, and the document restores itself to exactly how it was. The history keeps a pile of these snapshots, but it never looks inside them or changes them. It only stores them and hands them back.</p>
<p>That's the Memento pattern. It lets you capture and restore an object's state without exposing how that state is structured internally.</p>
<h4 id="heading-problems-it-solves">Problems it solves:</h4>
<ul>
<li><p>What if undo required exposing every private field of the document? That would break encapsulation, and any change to the document's internals would ripple out to whatever handles undo. The memento hides that structure inside an object only the document itself knows how to read.</p>
</li>
<li><p>What if the history needed to inspect or modify old snapshots? It shouldn't be able to. The caretaker only stores and returns mementos, it never reads or changes what's inside them.</p>
</li>
<li><p>What if you needed several restore points, not just one? Because each memento is just an object, they can be stacked, listed, or discarded, giving you as many restore points as you want to keep.</p>
</li>
</ul>
<p>In simple terms, you capture an object's state in a snapshot you can restore later, without exposing how that state is put together internally.</p>
<p>Wikipedia describes it like this:</p>
<blockquote>
<p><em>"The memento pattern is a software design pattern that provides the ability to restore an object to its previous state (undo via rollback)."</em></p>
<p><strong>(</strong><a href="https://en.wikipedia.org/wiki/Memento_pattern">Source</a>)</p>
</blockquote>
<h4 id="heading-programming-example">Programming Example:</h4>
<p><code>TextEditor</code> is the originator: it creates <code>EditorMemento</code> snapshots of itself and can restore from one. <code>History</code> is the caretaker: it stores mementos on a stack without ever looking inside them.</p>
<pre><code class="language-csharp">// The memento, an immutable snapshot of the editor's state
public class EditorMemento
{
    public string Content { get; }

    public EditorMemento(string content) =&gt; Content = content;
}
</code></pre>
<pre><code class="language-csharp">// The originator, creates and restores from mementos of its own state
public class TextEditor
{
    public string Content { get; private set; } = string.Empty;

    public void Write(string text) =&gt; Content += text;

    public EditorMemento Save() =&gt; new(Content);

    public void Restore(EditorMemento memento) =&gt; Content = memento.Content;
}
</code></pre>
<pre><code class="language-csharp">// The caretaker, stores mementos without ever looking inside them
public class History
{
    private readonly Stack&lt;EditorMemento&gt; _snapshots = new();

    public void Save(EditorMemento memento) =&gt; _snapshots.Push(memento);

    public EditorMemento Undo() =&gt; _snapshots.Pop();
}
</code></pre>
<p>Now let's see it in action:</p>
<pre><code class="language-csharp">var editor  = new TextEditor();
var history = new History();

editor.Write("Hello");
history.Save(editor.Save());

editor.Write(", world");
history.Save(editor.Save());

editor.Write("!!!");
Console.WriteLine($"Current: {editor.Content}");

editor.Restore(history.Undo());
Console.WriteLine($"After undo: {editor.Content}");

editor.Restore(history.Undo());
Console.WriteLine($"After undo: {editor.Content}");
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="language-yaml">Current: Hello, world!!!
After undo: Hello, world
After undo: Hello
</code></pre>
<p><code>History</code> never read or changed the text inside a snapshot. It just pushed mementos on and popped them off. Only <code>TextEditor</code> knew what to do with the content inside one. That's the Memento pattern.</p>
<h4 id="heading-when-to-use-it">When to Use it</h4>
<p>Use Memento when you need undo/redo functionality and want to capture state without exposing an object's internals.</p>
<p>It's also a good choice when taking a snapshot directly would break encapsulation by exposing private fields.</p>
<p>And it's helpful when you want the object that stores history to stay dumb: like holding snapshots without knowing or caring what's inside them.</p>
<h3 id="heading-7-the-observer-design-pattern">7. The Observer Design Pattern</h3>
<h4 id="heading-real-world-example">Real World Example</h4>
<p>Think of subscribing to a YouTube channel. You don't sit there refreshing the page, checking if a new video has been uploaded. You subscribe once, and the moment the channel uploads something, you get notified automatically.</p>
<p>The channel doesn't know or care what each subscriber does with that notification. It just knows it has a list of subscribers, and when something changes, it tells all of them.</p>
<p>That's the Observer pattern. One object holds a list of dependents, and whenever its state changes, it notifies every one of them automatically.</p>
<h4 id="heading-problems-it-solves">Problems it solves:</h4>
<ul>
<li><p>What if every subscriber had to keep checking the channel for updates? That would waste effort and add delay. The channel notifying its subscribers directly means they find out the moment it happens.</p>
</li>
<li><p>What if the channel had to know exactly what each subscriber wanted to do with a new video? It shouldn't need to. The channel only calls <code>Notify()</code>, each subscriber decides for itself what that means.</p>
</li>
<li><p>What if you wanted to add or remove subscribers at runtime? The channel doesn't need to change. It just keeps a list, and subscribing or unsubscribing only ever affects that list.</p>
</li>
</ul>
<p>In simple terms, one object keeps a list of dependents and automatically notifies all of them whenever its own state changes.</p>
<p>Wikipedia describes it like this:</p>
<blockquote>
<p><em>"The observer pattern is a software design pattern in which an object, named the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods."</em> <a href="https://en.wikipedia.org/wiki/Observer_pattern">(Source</a>)</p>
</blockquote>
<h4 id="heading-programming-example">Programming Example:</h4>
<p><code>YouTubeChannel</code> is the subject: it keeps a list of <code>ISubscriber</code>s and notifies all of them whenever a video is uploaded. <code>Subscriber</code> is the concrete observer, deciding for itself what to do with that notification.</p>
<pre><code class="language-csharp">// The observer interface, every subscriber implements this
public interface ISubscriber
{
    void Notify(string channelName, string videoTitle);
}
</code></pre>
<pre><code class="language-csharp">// The concrete observer
public class Subscriber : ISubscriber
{
    private readonly string _name;

    public Subscriber(string name) =&gt; _name = name;

    public void Notify(string channelName, string videoTitle)
    {
        Console.WriteLine($"{_name}: {channelName} just uploaded '{videoTitle}'!");
    }
}
</code></pre>
<pre><code class="language-csharp">// The subject, keeps track of its subscribers and notifies them of changes
public class YouTubeChannel
{
    private readonly string _name;
    private readonly List&lt;ISubscriber&gt; _subscribers = new();

    public YouTubeChannel(string name) =&gt; _name = name;

    public void Subscribe(ISubscriber subscriber)   =&gt; _subscribers.Add(subscriber);
    public void Unsubscribe(ISubscriber subscriber) =&gt; _subscribers.Remove(subscriber);

    public void UploadVideo(string title)
    {
        Console.WriteLine($"{_name}: Uploaded '{title}'.");

        foreach (var subscriber in _subscribers)
        {
            subscriber.Notify(_name, title);
        }
    }
}
</code></pre>
<p>Now let's see it in action:</p>
<pre><code class="language-csharp">var channel = new YouTubeChannel("Code With Isaiah");

var alice = new Subscriber("Alice");
var bob   = new Subscriber("Bob");

channel.Subscribe(alice);
channel.Subscribe(bob);

channel.UploadVideo("Design Patterns Explained");

channel.Unsubscribe(bob);
channel.UploadVideo("Understanding the Observer Pattern");
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="language-yaml">Code With Isaiah: Uploaded 'Design Patterns Explained'.
Alice: Code With Isaiah just uploaded 'Design Patterns Explained'!
Bob: Code With Isaiah just uploaded 'Design Patterns Explained'!
Code With Isaiah: Uploaded 'Understanding the Observer Pattern'.
Alice: Code With Isaiah just uploaded 'Understanding the Observer Pattern'!
</code></pre>
<p>Once Bob unsubscribed, he stopped hearing about new uploads entirely. The channel never singled him out, it just no longer had him on the list it notifies. That's the Observer pattern.</p>
<h4 id="heading-when-to-use-it">When to Use it</h4>
<p>Use Observer when a change to one object should automatically update an unknown number of others.</p>
<p>It's also useful when you want objects to stay loosely coupled: the subject only knows about an observer interface, never concrete details.</p>
<p>And it's a good option when the number of dependents can grow or shrink at runtime, such as subscribing and unsubscribing.</p>
<h3 id="heading-8-the-state-design-pattern">8. The State Design Pattern</h3>
<h4 id="heading-real-world-example">Real World Example</h4>
<p>Think of an online order moving through its lifecycle: pending, then shipped, then delivered. What "moving to the next step" actually means is different at every stage. From pending it means handing the package to a courier. From shipped it means marking it as received. From delivered, there's nowhere left to go. Rather than one giant method full of <code>if</code> checks for every possible stage, each stage can just know what comes after it.</p>
<p>That's the State pattern. The object's behaviour changes based on its current state, and each state knows how to transition to the next one.</p>
<h4 id="heading-problems-it-solves">Problems it solves:</h4>
<ul>
<li><p>What if one method had to handle every stage with a long chain of conditionals? It would grow harder to follow every time a new stage was added. Giving each stage its own class keeps the logic for that stage self-contained.</p>
</li>
<li><p>What if adding a new stage meant editing that same giant method? It's easy to introduce a bug in an unrelated stage while doing so. A new state is just a new class, dropped in alongside the others.</p>
</li>
<li><p>What if the object needed to behave completely differently depending on where it was in its lifecycle? Delegating to the current state object means the context doesn't need to know the details. It just asks the current state what to do.</p>
</li>
</ul>
<p>In simple terms, you let an object change its behaviour by changing which state object it's currently holding, so the object appears to change how it acts as its state changes.</p>
<p>Wikipedia describes it like this:</p>
<blockquote>
<p><em>"The state pattern is a behavioral software design pattern that allows an object to alter its behavior when its internal state changes. This pattern is close to the concept of finite-state machines."</em> <strong>(</strong><a href="https://en.wikipedia.org/wiki/State_pattern">Source</a>)</p>
</blockquote>
<h4 id="heading-programming-example">Programming Example:</h4>
<p><code>Order</code> is the context: it holds whatever <code>IOrderState</code> it's currently in and delegates to it. Each concrete state, <code>PendingState</code>, <code>ShippedState</code>, <code>DeliveredState</code>, knows what the next state should be.</p>
<pre><code class="language-csharp">// The state interface, every state implements this
public interface IOrderState
{
    void Next(Order order);
    string Name { get; }
}
</code></pre>
<pre><code class="language-csharp">// The context, delegates behaviour to whatever state it currently holds
public class Order
{
    public IOrderState State { get; set; } = new PendingState();

    public void Next()
    {
        Console.WriteLine($"Order is currently: {State.Name}");
        State.Next(this);
    }
}
</code></pre>
<pre><code class="language-csharp">// Concrete states, each knows what comes after it
public class PendingState : IOrderState
{
    public string Name =&gt; "Pending";

    public void Next(Order order) =&gt; order.State = new ShippedState();
}

public class ShippedState : IOrderState
{
    public string Name =&gt; "Shipped";

    public void Next(Order order) =&gt; order.State = new DeliveredState();
}

public class DeliveredState : IOrderState
{
    public string Name =&gt; "Delivered";

    public void Next(Order order)
    {
        Console.WriteLine("Order has already been delivered. Nothing left to do.");
    }
}
</code></pre>
<p>Now let's see it in action:</p>
<pre><code class="language-csharp">var order = new Order();

order.Next();
order.Next();
order.Next();
order.Next();
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="language-json">Order is currently: Pending
Order is currently: Shipped
Order is currently: Delivered
Order has already been delivered. Nothing left to do.
</code></pre>
<p><code>Order</code> never checked "if pending, do this, if shipped, do that." It just asked its current state what to do next, and the state itself decided what came after. That's the State pattern.</p>
<h4 id="heading-when-to-use-it">When to Use it</h4>
<p>Use State when an object's behaviour depends on its state, and it must change that behaviour at runtime as the state changes.</p>
<p>It's also helpful when you have large conditional blocks that branch on the object's current state or type.</p>
<p>And choose it when transitions between states should be explicit and self-contained, rather than scattered across one big method.</p>
<h3 id="heading-9-the-strategy-design-pattern">9. The Strategy Design Pattern</h3>
<h4 id="heading-real-world-example">Real World Example</h4>
<p>Think of checking out of an online store. You can pay by credit card, or you can pay through PayPal. The shopping cart doesn't care which one you pick. It just knows the total, hands it to whichever payment method you chose, and lets that method handle the details of actually charging you. Swap the payment method, and the cart's own code never changes.</p>
<p>That's the Strategy pattern. An algorithm (in this case "how to pay") is pulled out into its own interchangeable object, and the client just picks which one to use.</p>
<h4 id="heading-problems-it-solves">Problems it solves:</h4>
<ul>
<li><p>What if the cart had a big <code>if/else</code> for every payment method? Adding a new one would mean editing that method every time. Pulling each payment method out into its own class means the cart never needs to change.</p>
</li>
<li><p>What if you wanted to swap the algorithm at runtime? A hardcoded method can't be swapped. A strategy object can simply be replaced with another one that implements the same interface.</p>
</li>
<li><p>What if two different payment methods needed to share a common interface but nothing else? Each one implements the strategy interface, but its internal details (a card number here, an email there) stay private to it.</p>
</li>
</ul>
<p>In simple terms, you pull an algorithm out into its own interchangeable object, so the class using it doesn't need to know or care which specific version is running.</p>
<p>Wikipedia describes it like this:</p>
<blockquote>
<p><em>"The strategy pattern is a behavioral software design pattern that enables selecting an algorithm at runtime."</em> <a href="https://en.wikipedia.org/wiki/Strategy_pattern">(Source</a>)</p>
</blockquote>
<h4 id="heading-programming-example">Programming Example:</h4>
<p><code>ShoppingCart</code> is the context: it holds an <code>IPaymentStrategy</code> and delegates the actual payment to it. <code>CreditCardPayment</code> and <code>PayPalPayment</code> are concrete strategies, each a different way to pay.</p>
<pre><code class="language-csharp">// The strategy interface, every payment method implements this
public interface IPaymentStrategy
{
    void Pay(decimal amount);
}
</code></pre>
<pre><code class="language-csharp">// Concrete strategies, each a different way to pay
public class CreditCardPayment : IPaymentStrategy
{
    private readonly string _cardNumber;

    public CreditCardPayment(string cardNumber) =&gt; _cardNumber = cardNumber;

    public void Pay(decimal amount)
    {
        Console.WriteLine($"Charged ${amount} to credit card ending in {_cardNumber[^4..]}.");
    }
}

public class PayPalPayment : IPaymentStrategy
{
    private readonly string _email;

    public PayPalPayment(string email) =&gt; _email = email;

    public void Pay(decimal amount)
    {
        Console.WriteLine($"Charged ${amount} via PayPal account {_email}.");
    }
}
</code></pre>
<pre><code class="language-csharp">// The context, holds a strategy and delegates the actual payment work to it
public class ShoppingCart
{
    private readonly decimal _total;
    private IPaymentStrategy? _paymentMethod;

    public ShoppingCart(decimal total) =&gt; _total = total;

    public void SetPaymentMethod(IPaymentStrategy method) =&gt; _paymentMethod = method;

    public void Checkout()
    {
        if (_paymentMethod is null)
        {
            Console.WriteLine("No payment method selected.");
            return;
        }

        _paymentMethod.Pay(_total);
    }
}
</code></pre>
<p>Now let's see it in action:</p>
<pre><code class="language-csharp">var cart = new ShoppingCart(59.99m);

cart.SetPaymentMethod(new CreditCardPayment("4111 1111 1111 1111"));
cart.Checkout();

cart.SetPaymentMethod(new PayPalPayment("isaiah@example.com"));
cart.Checkout();
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="language-json">Charged $59.99 to credit card ending in 1111.
Charged $59.99 via PayPal account isaiah@example.com.
</code></pre>
<p><code>ShoppingCart</code> never knew how a payment actually got processed. It just called <code>Pay()</code> on whatever strategy it was holding at the time. That's the Strategy pattern: the algorithm is swapped out, the class using it stays exactly the same.</p>
<h4 id="heading-when-to-use-it">When to Use it</h4>
<p>Use Strategy when you have several variants of an algorithm, and want to switch between them at runtime.</p>
<p>It's also helpful when you want to avoid a class full of conditionals that pick behaviour based on a type or flag.</p>
<p>And it's a solid choice when related classes only differ in the behaviour they use, and that behaviour should be interchangeable.</p>
<h3 id="heading-10-the-template-method-design-pattern">10. The Template Method Design Pattern</h3>
<h4 id="heading-real-world-example">Real World Example</h4>
<p>Think of making a hot drink, tea or coffee. Both follow the exact same basic steps: boil water, brew, pour into a cup, and add something to taste. What differs is only two of those steps: tea gets steeped, and coffee gets brewed through grounds. Tea gets lemon, and coffee gets sugar and milk. The overall recipe never changes, only the specific details of a couple of steps within it.</p>
<p>That's the Template Method pattern. A base class defines the fixed skeleton of an algorithm, and subclasses only fill in the steps that are actually allowed to vary.</p>
<h4 id="heading-problems-it-solves">Problems it solves:</h4>
<ul>
<li><p>What if every beverage repeated the entire recipe from scratch? Boiling water and pouring into a cup would be duplicated in every single class. The template method keeps those steps in one place, written once.</p>
</li>
<li><p>What if a subclass could reorder the steps, or skip one entirely? That would let each beverage break the overall recipe. Because the algorithm's skeleton lives in the base class as a single method, the order and structure stay fixed.</p>
</li>
<li><p>What if you wanted to add a new beverage? Only the steps that differ, brewing and condiments, need to be written. Everything else is already handled by the base class.</p>
</li>
</ul>
<p>In simple terms, you define the fixed skeleton of an algorithm in a base class, and let subclasses fill in only the steps that are actually allowed to differ.</p>
<p>Wikipedia describes it like this:</p>
<blockquote>
<p><em>"In object-oriented programming, the template method is one of the behavioral design patterns identified by Gamma et al. in the book Design Patterns. The template method is a method in a superclass, usually an abstract superclass, and defines the skeleton of an operation in terms of a number of high-level steps." (</em><a href="https://en.wikipedia.org/wiki/Template_method_pattern">Source</a>)</p>
</blockquote>
<h4 id="heading-programming-example">Programming Example:</h4>
<p><code>Beverage</code> defines <code>Prepare()</code> as the template method: the fixed sequence of steps. <code>Tea</code> and <code>Coffee</code> only override <code>Brew()</code> and <code>AddCondiments()</code>, the two steps that are actually allowed to vary.</p>
<pre><code class="language-csharp">// The abstract class, defines the skeleton of the algorithm
public abstract class Beverage
{
    // The template method, the steps and their order never change
    public void Prepare()
    {
        BoilWater();
        Brew();
        PourInCup();
        AddCondiments();
    }

    private void BoilWater() =&gt; Console.WriteLine("Boiling water.");
    private void PourInCup() =&gt; Console.WriteLine("Pouring into cup.");

    // Steps left for subclasses to fill in
    protected abstract void Brew();
    protected abstract void AddCondiments();
}
</code></pre>
<pre><code class="language-csharp">// A concrete class, fills in the steps specific to tea
public class Tea : Beverage
{
    protected override void Brew() =&gt; Console.WriteLine("Steeping the tea bag.");
    protected override void AddCondiments() =&gt; Console.WriteLine("Adding lemon.");
}

// Another concrete class, fills in the steps specific to coffee
public class Coffee : Beverage
{
    protected override void Brew() =&gt; Console.WriteLine("Brewing the coffee grounds.");
    protected override void AddCondiments() =&gt; Console.WriteLine("Adding sugar and milk.");
}
</code></pre>
<p>Now let's see it in action:</p>
<pre><code class="language-csharp">Beverage tea    = new Tea();
Beverage coffee = new Coffee();

tea.Prepare();
Console.WriteLine();
coffee.Prepare();
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="language-yaml">Boiling water.
Steeping the tea bag.
Pouring into cup.
Adding lemon.

Boiling water.
Brewing the coffee grounds.
Pouring into cup.
Adding sugar and milk.
</code></pre>
<p>Both drinks boiled water and poured into a cup in exactly the same way, because <code>Prepare()</code> in the base class handled that. Only brewing and condiments changed, because those were the steps each subclass was actually responsible for. That's the Template Method pattern.</p>
<h4 id="heading-when-to-use-it">When to Use it</h4>
<p>Use Template Method when several classes share the same overall algorithm, but differ in a few specific steps.</p>
<p>It's a good choice when you want to enforce a fixed sequence of steps, while still letting subclasses customise parts of it.</p>
<p>And it's helpful when you want to avoid duplicating the parts of an algorithm that never change across every subclass.</p>
<h3 id="heading-11-the-visitor-design-pattern">11. The Visitor Design Pattern</h3>
<h4 id="heading-real-world-example">Real World Example</h4>
<p>Think of a shopping cart with different kinds of items: books and electronics, each taxed differently at checkout. You don't want to bake pricing logic into the <code>Book</code> and <code>Electronic</code> classes themselves, especially if you'll need other operations on them later too, like generating a shipping label or a warranty summary. Instead, each item just accepts a visitor and hands itself over. The visitor is the one that actually knows how to price a book differently from an electronic.</p>
<p>That's the Visitor pattern. The operation lives outside the objects it acts on, and each object just lets the visitor know what it needs to know: what type of thing it actually is.</p>
<h4 id="heading-problems-it-solves">Problems it solves:</h4>
<ul>
<li><p>What if pricing logic was written directly inside <code>Book</code> and <code>Electronic</code>? Every new operation (tax, shipping, warranty) would mean editing both classes again and again. The visitor keeps each new operation in its own self-contained class instead.</p>
</li>
<li><p>What if you needed to add a new operation without touching the existing item classes? Normally that means modifying every class the operation applies to. A new visitor is a new class, while <code>Book</code> and <code>Electronic</code> never change.</p>
</li>
<li><p>What if a generic loop had to guess the concrete type of each item? That usually means a chain of type checks. <code>Accept()</code> calling <code>Visit(this)</code> lets the compiler pick the right overload automatically, without a single <code>if</code> or type check.</p>
</li>
</ul>
<p>In simple terms, you move an operation out of the objects it acts on and into its own class. Each object just accepts a visitor and lets it know what concrete type it is.</p>
<p>Wikipedia describes it like this:</p>
<blockquote>
<p><em>"The visitor design pattern is a way of separating an algorithm from an object structure on which it operates." (</em><a href="https://en.wikipedia.org/wiki/Visitor_pattern">Source</a>)</p>
</blockquote>
<h4 id="heading-programming-example">Programming Example:</h4>
<p><code>Book</code> and <code>Electronic</code> both implement <code>IItem</code> and simply call <code>visitor.Visit(this)</code>. <code>PricingVisitor</code> implements <code>IVisitor</code> with an overload for each concrete type, so the right pricing logic runs automatically.</p>
<pre><code class="language-csharp">// The element interface, every item in the cart implements this
public interface IItem
{
    void Accept(IVisitor visitor);
}
</code></pre>
<pre><code class="language-csharp">// Concrete elements, each accepts a visitor and hands itself over
public class Book : IItem
{
    public string Title { get; }
    public decimal Price { get; }

    public Book(string title, decimal price)
    {
        Title = title;
        Price = price;
    }

    public void Accept(IVisitor visitor) =&gt; visitor.Visit(this);
}

public class Electronic : IItem
{
    public string Name { get; }
    public decimal Price { get; }

    public Electronic(string name, decimal price)
    {
        Name  = name;
        Price = price;
    }

    public void Accept(IVisitor visitor) =&gt; visitor.Visit(this);
}
</code></pre>
<pre><code class="language-csharp">// The visitor interface, one Visit overload per concrete element
public interface IVisitor
{
    void Visit(Book book);
    void Visit(Electronic electronic);
}
</code></pre>
<pre><code class="language-csharp">// A concrete visitor, adds a new operation without touching Book or Electronic
public class PricingVisitor : IVisitor
{
    public decimal Total { get; private set; }

    public void Visit(Book book)
    {
        Console.WriteLine($"Book: {book.Title} — ${book.Price:F2} (no tax).");
        Total += book.Price;
    }

    public void Visit(Electronic electronic)
    {
        var priceWithTax = electronic.Price * 1.15m;
        Console.WriteLine($"Electronic: {electronic.Name} — ${priceWithTax:F2} (with 15% tax).");
        Total += priceWithTax;
    }
}
</code></pre>
<p>Now let's see it in action:</p>
<pre><code class="language-csharp">var cart = new List&lt;IItem&gt;
{
    new Book("Design Patterns", 45.00m),
    new Electronic("Headphones", 120.00m)
};

var pricingVisitor = new PricingVisitor();

foreach (var item in cart)
{
    item.Accept(pricingVisitor);
}

Console.WriteLine($"Total: ${pricingVisitor.Total:F2}");
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="language-yaml">Book: Design Patterns — $45.00 (no tax).
Electronic: Headphones — $138.00 (with 15% tax).
Total: $183.00
</code></pre>
<p>Neither <code>Book</code> nor <code>Electronic</code> contained a single line of pricing logic. Each one only knew how to <code>Accept()</code> a visitor. <code>PricingVisitor</code> was the one that actually decided how each type gets priced. That's the Visitor pattern: the operation lives outside the object structure, not inside it.</p>
<h4 id="heading-when-to-use-it">When to Use it</h4>
<p>Use Visitor when you need to perform operations across a group of unrelated classes, without polluting each class with that logic.</p>
<p>It's also helpful when you want to add new operations often, but the object structure itself rarely changes.</p>
<p>And it's a good choice when you'd otherwise need type checks or casting to figure out what to do with each object in a collection.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>That covers all 23 classic design patterns across the three families: Creational, Structural, and Behavioral.</p>
<p>None of them are rules you must follow. They're answers to problems that show up again and again in software: how to create objects without hard-coding their exact type, how to compose bigger structures out of smaller ones, and how to let objects communicate without being tightly bound to each other.</p>
<p>A few things worth remembering:</p>
<ul>
<li><p>You won't use most of these patterns most of the time. Recognising <em>when a problem calls for one</em> is the actual skill. Forcing a pattern onto a problem that doesn't need it usually makes the code harder to follow, not easier.</p>
</li>
<li><p>The real world analogies exist to build intuition, not to be taken literally. Once a pattern's shape clicks in a story you understand, spotting it in real code becomes far easier.</p>
</li>
<li><p>Patterns compose. A Factory Method might produce objects that are themselves Decorators. A Composite tree might be built with a Builder. Real systems mix and layer patterns rather than using them in isolation.</p>
</li>
<li><p>The language doesn't matter. Every example here is in C#, but the same shapes exist in Python, Java, TypeScript, Go, Rust, and beyond. If you understand the <em>problem</em> a pattern solves, translating it to any language is straightforward.</p>
</li>
</ul>
<p>The goal isn't to memorise 23 names. It's to recognise the recurring problems underneath them, so that when one shows up in your own code, you already know a proven shape for solving it.</p>
<blockquote>
<p><em>"Each pattern describes a problem which occurs over and over again in our environment, and then describes the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice."</em></p>
<p><strong>Source:</strong> Christopher Alexander, <em>A Pattern Language</em> — the architectural work that originally inspired software design patterns.</p>
</blockquote>
<p>If this handbook was useful, the source lives at <a href="https://github.com/Clifftech123/design-patterns-handbook">github.com/Clifftech123/design-patterns-handbook</a>. Star it, fork it, or open a PR with a pattern you think is missing.</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[ Flutter Frontend Systems Design: How to Think Like a Senior Engineer in the AI Age ]]>
                </title>
                <description>
                    <![CDATA[ Systems design has always been treated as a backend problem. Ask a group of Flutter engineers what systems design means, and most will describe server architecture: load balancers, databases, and micr ]]>
                </description>
                <link>https://www.freecodecamp.org/news/flutter-frontend-systems-design-how-to-think-like-a-senior-engineer-in-the-ai-age/</link>
                <guid isPermaLink="false">6a79dcd1e93f9db759fd99d6</guid>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ System Design ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mobile app development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Dart ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software architecture ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Riverpod ]]>
                    </category>
                
                    <category>
                        <![CDATA[ interview-prep ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Jesutoni Aderibigbe ]]>
                </dc:creator>
                <pubDate>Mon, 10 Aug 2026 14:14:41 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/682cb489-c8fd-4530-9226-357edb4e8c19.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Systems design has always been treated as a backend problem.</p>
<p>Ask a group of Flutter engineers what systems design means, and most will describe server architecture: load balancers, databases, and microservices.</p>
<p>Ask them to design a distributed cache or sketch out a message queue, and they'll hesitate. Ask them to design the Flutter client for a social feed, and they'll open a new file and start writing widgets.</p>
<p>That's the gap. And it's closing fast.</p>
<p>As Flutter applications grow more complex with real-time features, offline support, multiple platform targets, and AI-generated code that still needs to be maintainable, the architectural decisions you make before writing a single widget become just as important as your backend architecture.</p>
<p>Senior Flutter interviews at product companies increasingly test this skill. The engineers who can clearly explain <em>why</em> they chose a particular architecture, the trade-offs they considered, and the problems they were optimizing for are the ones who get hired and promoted.</p>
<p>This article is structured in two halves. The first half explains what frontend systems design actually is and why it matters for Flutter engineers specifically in 2026. The second half works through a full mock interview answer for one of the most common scenario questions: designing the Flutter architecture for a social feed with infinite scroll, likes, comments, and real-time updates. We'll walk through the kind of answer that separates mid-level from senior in an interview room.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>This article assumes you're a working Flutter developer comfortable with state management (Riverpod, Bloc, or similar), REST APIs, and basic Dart. You don't need backend experience, but familiarity with concepts like caching, pagination, and WebSockets will help you follow the deeper sections.</p>
<p>No code setup is required. This is a thinking and architecture article, not a tutorial. Dart/Flutter snippets are used to ground abstract ideas in concrete implementation.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-1-what-is-frontend-systems-design">1. What is Frontend Systems Design?</a></p>
</li>
<li><p><a href="#heading-2-why-flutter-engineers-cant-ignore-it-anymore">2. Why Flutter Engineers Can't Ignore It Anymore</a></p>
</li>
<li><p><a href="#heading-3-the-interview-format-what-to-expect">3. The Interview Format: What to Expect</a></p>
</li>
<li><p><a href="#heading-4-how-to-structure-your-answer">4. How to Structure Your Answer</a></p>
</li>
<li><p><a href="#heading-5-mock-interview-design-a-social-feed">5. Mock Interview: Design a Social Feed</a></p>
</li>
<li><p><a href="#heading-6-other-questions-to-prepare-for">6. Other Questions to Prepare For</a></p>
</li>
<li><p><a href="#heading-7-key-takeaways">7. Key Takeaways</a></p>
</li>
</ul>
<h2 id="heading-1-what-is-frontend-systems-design">1. What is Frontend Systems Design?</h2>
<p>Systems design is the practice of making high-level decisions about how a software system is structured before implementation begins: how its components are divided, how they communicate, how it handles scale, failure, and change over time.</p>
<p>On the backend, this means deciding between microservices and a monolith, choosing a database, designing an API contract, and planning for horizontal scaling. The feedback loop is fast: a bad database schema causes slow queries within days, and a poorly designed API breaks clients immediately.</p>
<p>On the frontend, the consequences of bad design are slower and quieter. A 600-line screen widget still ships. A god-class repository with 40 methods still works. State leaks between sessions only surface after a frustrated user reports it.</p>
<p>Frontend systems design asks the same category of questions, applied to the client layer:</p>
<ul>
<li><p>How do you divide a large app into independently-buildable features?</p>
</li>
<li><p>Where does business logic live, and what enforces that boundary?</p>
</li>
<li><p>How does data flow from the network to the screen and back?</p>
</li>
<li><p>What happens when the network fails, the API changes shape, or the user logs out mid-session?</p>
</li>
<li><p>How do you design components that can be tested in isolation?</p>
</li>
<li><p>How do you structure the app so a team of engineers can work on it without stepping on each other?</p>
</li>
</ul>
<p>These aren't widget questions. They're architecture questions. And they have answers: principled ones, with real tradeoffs.</p>
<h2 id="heading-2-why-flutter-engineers-cant-ignore-it-anymore">2. Why Flutter Engineers Can't Ignore It Anymore</h2>
<p>Three forces are pushing systems design into the Flutter conversation in a way that simply didn't exist three years ago.</p>
<h3 id="heading-flutter-apps-are-no-longer-just-uis">Flutter Apps Are No Longer Just UIs</h3>
<p>With Serverpod and Dart Frog on the server, Jaspr on the web, and Flutter on mobile and desktop, Dart is now a genuinely full-stack language. Engineers making architecture decisions that span mobile, web, and server in the same codebase need systems thinking, not just widget composition skills.</p>
<p>When your Freezed model is shared between the Flutter client and the Dart backend, the boundary between "frontend" and "backend" design dissolves. You're designing a system.</p>
<h3 id="heading-ai-agents-expose-bad-architecture-immediately">AI Agents Expose Bad Architecture Immediately</h3>
<p>This is the new pressure point. When Claude Code or any AI coding agent reads your project cold, it has no accumulated mental model to compensate for messiness. It reads files sequentially. It works within a limited context window. It makes decisions based on the patterns it sees.</p>
<p>A codebase with tangled dependencies, inconsistent naming, and business logic scattered across the widget tree produces unreliable AI output. This doesn't happen because the AI is wrong, but because the code doesn't communicate its own structure clearly enough to be navigated by something without human intuition.</p>
<p>Good systems design and AI-navigable architecture are almost identical. Feature-first structure, clear layer boundaries, consistent naming, small, focused files. These aren't just team hygiene practices anymore. They're what make AI-assisted development actually work at scale.</p>
<h3 id="heading-senior-flutter-interviews-now-test-it-explicitly">Senior Flutter Interviews Now Test it Explicitly</h3>
<p>As Flutter matures and product companies build larger apps with larger teams, the interview bar has risen. A mid-level Flutter interview might test widget lifecycle and state management fundamentals. A senior interview tests your ability to design a system you've never seen before, live, under pressure, while explaining your thinking out loud.</p>
<p>If you haven't thought about this before walking into that room, you'll be caught off guard.</p>
<h2 id="heading-3-the-interview-format-what-to-expect">3. The Interview Format: What to Expect</h2>
<p>Frontend systems design interviews at senior level typically run 45–60 minutes. You're given a vague scenario, like "design the <strong>Flutter client for a social feed"</strong>, and you're expected to drive the conversation.</p>
<p>The interviewer isn't looking for a single correct answer. They're watching how you think:</p>
<ul>
<li><p>Do you clarify requirements before jumping to solutions?</p>
</li>
<li><p>Do you identify the hard problems (real-time sync, optimistic UI, offline states) rather than the easy ones?</p>
</li>
<li><p>Do you make tradeoffs explicitly rather than just picking the thing you know best?</p>
</li>
<li><p>Can you go deep on any layer when pushed?</p>
</li>
</ul>
<p>The biggest mistake candidates make is opening Xcode or a code file immediately and starting to build. Systems design interviews are whiteboard conversations, not implementation sessions. Draw boxes. Name the layers. Talk through the data flow before writing a single method signature.</p>
<h2 id="heading-4-how-to-structure-your-answer">4. How to Structure Your Answer</h2>
<p>Use this framework for any frontend systems design question:</p>
<ol>
<li><p><strong>Clarify requirements (5 minutes)</strong> What platforms? How many users? Offline support? Real-time? Authentication? What's in scope for this conversation? Never assume.</p>
</li>
<li><p><strong>Define the data model (5–10 minutes)</strong> What are the core entities? What are their relationships? This anchors every architectural decision that follows.</p>
</li>
<li><p><strong>Design the layer architecture (10 minutes)</strong> How is the app divided? What are the layers? What enforces the boundaries between them?</p>
</li>
<li><p><strong>Solve the hard problems one by one (20–25 minutes)</strong> Pagination. Optimistic UI. Real-time sync. Offline. Performance. Go deep on each one, and name the tradeoffs.</p>
</li>
<li><p><strong>Address failure states (5 minutes)</strong> What breaks? What's the user experience when it does? Senior answers always include error handling.</p>
</li>
<li><p><strong>Summarise and invite questions (5 minutes)</strong> Recap the key decisions and the tradeoffs you made. Show you can hold the whole picture.</p>
</li>
</ol>
<h2 id="heading-5-mock-interview-design-a-social-feed">5. Mock Interview: Design a Social Feed</h2>
<blockquote>
<p><strong>Interviewer:</strong> Design the Flutter client architecture for a social feed. Users can scroll through posts, like and comment on them, and receive real-time updates when new posts arrive.</p>
</blockquote>
<p>This is the answer.</p>
<h3 id="heading-step-1-clarify-requirements">Step 1: Clarify Requirements</h3>
<p>Before touching architecture, ask the questions that constrain your decisions.</p>
<blockquote>
<p><em>"A few questions before I start. What platforms are we targeting? Mobile only, or web and desktop too? How many users are we designing for? Is this a startup MVP or an app at scale? Do we need offline support? How real-time does real-time need to be? Are we talking push notifications, or should the feed update while the user is looking at it? And what's the authentication model? Are users logged in, or is there a guest mode?"</em></p>
</blockquote>
<p>For this walkthrough, assume:</p>
<ul>
<li><p>Mobile (iOS + Android), with web on the roadmap</p>
</li>
<li><p>Tens of thousands of MAU. Not Twitter scale, but meaningful.</p>
</li>
<li><p>Offline: show cached content, queue interactions</p>
</li>
<li><p>Real-time: live feed updates while the screen is open (WebSocket)</p>
</li>
<li><p>Auth: logged-in users only</p>
</li>
</ul>
<p>These answers change every architectural decision that follows. Offline support means a local cache layer. Live updates while the screen is open means WebSockets, not polling. Web on the roadmap means avoiding anything mobile-only in the business logic layer.</p>
<h3 id="heading-step-2-define-the-data-model">Step 2: Define the Data Model</h3>
<p>Start with the entities and their relationships. Draw these before writing any code.</p>
<pre><code class="language-dart">// Core entities

@freezed
class Post with _$Post {
  const factory Post({
    required String id,
    required String authorId,
    required String authorName,
    required String authorAvatarUrl,
    required String content,
    String? imageUrl,
    required int likeCount,
    required int commentCount,
    required bool isLikedByMe,      // derived from current user context
    required DateTime createdAt,
  }) = _Post;
}

@freezed
class Comment with _$Comment {
  const factory Comment({
    required String id,
    required String postId,
    required String authorId,
    required String authorName,
    required String content,
    required DateTime createdAt,
  }) = _Comment;
}

@freezed
class FeedPage with _$FeedPage {
  const factory FeedPage({
    required List&lt;Post&gt; posts,
    required String? nextCursor,    // null = end of feed
  }) = _FeedPage;
}
</code></pre>
<p>A few design decisions embedded in this model are worth calling out explicitly in an interview:</p>
<p><code>isLikedByMe</code> <strong>lives on the Post.</strong> You could derive this from a separate user-likes table, but embedding it in the post response is simpler and makes the UI stateless. The screen doesn't need to join two data sources to render a like button.</p>
<p><strong>Cursor-based pagination, not offset.</strong> <code>nextCursor</code> rather than <code>page: 2</code>. Offset pagination breaks when new posts are inserted at the top. Item 20 on page 2 becomes item 21, and you either show a duplicate or skip an item. Cursors are stable.</p>
<p><code>likeCount</code> <strong>and</strong> <code>commentCount</code> <strong>are integers, not arrays.</strong> You don't fetch all likers to render a post. You fetch the count and a flag. This is a deliberate API contract decision that prevents unbounded payload size.</p>
<h3 id="heading-step-3-design-the-layer-architecture">Step 3: Design the Layer Architecture</h3>
<p>A feed is a good test of layer discipline because data flows in multiple directions: down from the API, up from user interactions, and sideways from real-time events. A flat architecture collapses quickly.</p>
<p>Here's the structure:</p>
<pre><code class="language-plaintext">lib/
├── core/
│   ├── network/          # Dio client, interceptors, token refresh
│   ├── cache/            # Local storage abstraction (Hive or Isar)
│   ├── realtime/         # WebSocket connection manager
│   └── errors/           # Typed error classes
└── features/
    └── feed/
        ├── data/
        │   ├── models/   # Post, Comment, FeedPage (Freezed)
        │   ├── sources/
        │   │   ├── feed_remote_source.dart   # API calls
        │   │   └── feed_local_source.dart    # Cache reads/writes
        │   └── repositories/
        │       └── feed_repository.dart      # Coordinates remote + local
        └── presentation/
            ├── screens/
            │   └── feed_screen.dart
            ├── widgets/
            │   ├── post_card.dart
            │   ├── like_button.dart
            │   └── comment_sheet.dart
            └── providers/
                ├── feed_provider.dart        # Paginated post list
                ├── like_provider.dart        # Like/unlike actions
                └── realtime_provider.dart    # WebSocket events → state
</code></pre>
<p>A couple things worth noting here:</p>
<p>First, the repository is the only component that talks to both the remote source and the local source. Providers call the repository. The repository decides whether to hit the network or return cached data. Screens never know the data came from cache.</p>
<p>Second, the real-time layer is separate from the data fetching layer. It's a common mistake to wire WebSocket events directly into the same provider that manages pagination, and it becomes impossible to test or reason about. The <code>realtime_provider</code> receives events and patches the feed state and the <code>feed_provider</code> manages the paginated list. They coordinate through Riverpod's <code>ref</code>, not through direct dependency.</p>
<h3 id="heading-step-4-pagination-and-infinite-scroll">Step 4: Pagination and Infinite Scroll</h3>
<p>Infinite scroll is the first hard problem. The naïve implementation: a <code>ListView</code> that loads everything falls apart at a few hundred posts.</p>
<p>Here's a Riverpod <code>AsyncNotifier</code> that handles cursor-based pagination:</p>
<pre><code class="language-dart">@riverpod
class FeedNotifier extends _$FeedNotifier {
  static const _pageSize = 20;
  String? _nextCursor;
  bool _isFetchingMore = false;

  @override
  Future&lt;List&lt;Post&gt;&gt; build() async {
    // Load first page + seed from cache if available
    final cached = await ref.read(feedLocalSourceProvider).getCachedPosts();
    if (cached.isNotEmpty) {
      // Show cache immediately, refresh in background
      _refreshInBackground();
      return cached;
    }
    return _fetchPage(cursor: null);
  }

  Future&lt;void&gt; loadMore() async {
    if (_isFetchingMore || _nextCursor == null) return;
    _isFetchingMore = true;

    final currentPosts = state.valueOrNull ?? [];
    final page = await ref
        .read(feedRepositoryProvider)
        .getFeedPage(cursor: _nextCursor, limit: _pageSize);

    _nextCursor = page.nextCursor;
    state = AsyncData([...currentPosts, ...page.posts]);
    _isFetchingMore = false;
  }

  Future&lt;List&lt;Post&gt;&gt; _fetchPage({required String? cursor}) async {
    final page = await ref
        .read(feedRepositoryProvider)
        .getFeedPage(cursor: cursor, limit: _pageSize);
    _nextCursor = page.nextCursor;
    await ref.read(feedLocalSourceProvider).cachePosts(page.posts);
    return page.posts;
  }

  void _refreshInBackground() {
    Future.microtask(() async {
      final freshPosts = await _fetchPage(cursor: null);
      state = AsyncData(freshPosts);
    });
  }

  bool get hasMore =&gt; _nextCursor != null;
}
</code></pre>
<p>In the screen, trigger <code>loadMore()</code> before the user reaches the bottom, not at the last item, but a few items before it:</p>
<pre><code class="language-dart">NotificationListener&lt;ScrollNotification&gt;(
  onNotification: (notification) {
    if (notification.metrics.pixels &gt;
        notification.metrics.maxScrollExtent - 400) {
      ref.read(feedNotifierProvider.notifier).loadMore();
    }
    return false;
  },
  child: ListView.builder(
    itemCount: posts.length + (hasMore ? 1 : 0),
    itemBuilder: (context, index) {
      if (index == posts.length) return const FeedLoadingIndicator();
      return PostCard(post: posts[index]);
    },
  ),
)
</code></pre>
<p>The 400-pixel threshold means the next page starts loading before the user sees the end of the list. The experience feels seamless.</p>
<h3 id="heading-step-5-optimistic-ui-for-likes-and-comments">Step 5: Optimistic UI for Likes and Comments</h3>
<p>Optimistic UI is the practice of updating the local state immediately when a user takes an action, before the server confirms it, then rolling back if the server rejects it. It's what makes a like button feel instant rather than laggy.</p>
<p>The pattern has three steps: apply the optimistic update, fire the network request, and roll back on failure.</p>
<pre><code class="language-dart">@riverpod
class LikeNotifier extends _$LikeNotifier {
  @override
  void build() {}

  Future&lt;void&gt; toggleLike(String postId) async {
    final feedNotifier = ref.read(feedNotifierProvider.notifier);
    final currentPosts = ref.read(feedNotifierProvider).valueOrNull ?? [];

    // Find the post
    final postIndex = currentPosts.indexWhere((p) =&gt; p.id == postId);
    if (postIndex == -1) return;
    final post = currentPosts[postIndex];

    // Step 1: Apply optimistic update immediately
    final optimisticPost = post.copyWith(
      isLikedByMe: !post.isLikedByMe,
      likeCount: post.isLikedByMe ? post.likeCount - 1 : post.likeCount + 1,
    );
    feedNotifier.patchPost(postIndex, optimisticPost);

    // Step 2: Fire the network request
    try {
      await ref.read(feedRepositoryProvider).toggleLike(postId);
    } catch (e) {
      // Step 3: Roll back on failure
      feedNotifier.patchPost(postIndex, post);
      // Show a snackbar or error indicator
    }
  }
}
</code></pre>
<p>The <code>patchPost</code> method on <code>FeedNotifier</code> replaces a single post in the list without rebuilding the whole feed. This is an important performance detail when the list has hundreds of items.</p>
<p><strong>The tradeoff to name explicitly in an interview:</strong> optimistic UI can produce an inconsistent state if the server is the source of truth for like counts. Two users liking simultaneously might both see their local count increment from 41 to 42, but the real count is 43. For a social app, this is usually acceptable. You show the user their action was registered, and the next feed refresh corrects the count. For financial transactions, an optimistic UI is inappropriate. Know where to draw the line.</p>
<h3 id="heading-step-6-real-time-updates">Step 6: Real-Time Updates</h3>
<p>Real-time feed updates and new posts appearing while the user is looking at the screen require a persistent connection. WebSocket is the right tool here. Server-Sent Events work too, but WebSocket is bidirectional, which matters if you later want to push events (typing indicators, presence).</p>
<p>Design the WebSocket layer as a singleton service, not inside the feed feature:</p>
<pre><code class="language-dart">// core/realtime/realtime_service.dart

class RealtimeService {
  WebSocketChannel? _channel;
  final _controller = StreamController&lt;RealtimeEvent&gt;.broadcast();

  Stream&lt;RealtimeEvent&gt; get events =&gt; _controller.stream;

  Future&lt;void&gt; connect(String token) async {
    _channel = WebSocketChannel.connect(
      Uri.parse('wss://api.yourapp.com/ws?token=$token'),
    );

    _channel!.stream.listen(
      (data) {
        final event = RealtimeEvent.fromJson(jsonDecode(data as String));
        _controller.add(event);
      },
      onError: (_) =&gt; _scheduleReconnect(),
      onDone: () =&gt; _scheduleReconnect(),
    );
  }

  void _scheduleReconnect() {
    Future.delayed(const Duration(seconds: 3), connect);
  }

  void dispose() {
    _channel?.sink.close();
    _controller.close();
  }
}
</code></pre>
<p>Then in the feed layer, listen to the stream and patch state when new posts arrive:</p>
<pre><code class="language-dart">@riverpod
class RealtimeFeedNotifier extends _$RealtimeFeedNotifier {
  StreamSubscription? _subscription;

  @override
  void build() {
    _subscription = ref
        .read(realtimeServiceProvider)
        .events
        .where((e) =&gt; e.type == RealtimeEventType.newPost)
        .listen((event) {
      final newPost = Post.fromJson(event.payload);
      ref.read(feedNotifierProvider.notifier).prependPost(newPost);
    });

    ref.onDispose(() =&gt; _subscription?.cancel());
  }
}
</code></pre>
<p><strong>The UX decision worth raising in an interview:</strong> do you silently prepend new posts to the top of the feed, or do you show a "3 new posts, tap to refresh" banner?</p>
<p>Silent prepend is jarring: the user is reading post 5, and suddenly they're reading post 8. The banner pattern (used by Twitter/X and LinkedIn) is almost always the better choice. It signals freshness without disrupting reading position.</p>
<h3 id="heading-step-7-offline-and-error-states">Step 7: Offline and Error States</h3>
<p>An offline-capable feed has two distinct requirements: show something useful when there's no connection, and queue interactions (likes, comments) so they fire when connectivity returns.</p>
<p>For showing cached content, the repository pattern handles this cleanly:</p>
<pre><code class="language-dart">// feed_repository.dart

Future&lt;List&lt;Post&gt;&gt; getFeed({String? cursor}) async {
  try {
    final page = await _remoteSource.getFeedPage(cursor: cursor);
    await _localSource.cachePosts(page.posts);
    return page.posts;
  } on DioException catch (e) {
    if (e.type == DioExceptionType.connectionError) {
      // Network unavailable — return cache
      final cached = await _localSource.getCachedPosts();
      if (cached.isNotEmpty) return cached;
    }
    rethrow;
  }
}
</code></pre>
<p>For queuing interactions offline, keep a simple pending actions queue in local storage:</p>
<pre><code class="language-dart">@freezed
class PendingAction with _$PendingAction {
  const factory PendingAction.like({
    required String postId,
    required bool isLike,
    required DateTime queuedAt,
  }) = PendingLike;

  const factory PendingAction.comment({
    required String postId,
    required String content,
    required DateTime queuedAt,
  }) = PendingComment;
}
</code></pre>
<p>When connectivity returns (detected via <code>connectivity_plus</code>), drain the queue and fire each action in order. If an action fails after retry, surface it to the user. Don't silently drop it.</p>
<h3 id="heading-step-8-performance-considerations">Step 8: Performance Considerations</h3>
<p>A feed is one of the most performance-sensitive screens in any app. There are a few non-negotiable practices:</p>
<p>First, use <code>ListView.builder</code>, never <code>ListView</code> with a <code>children</code> list. Builder renders only the items currently on screen. A <code>children</code> list renders all of them at once (which would be catastrophic for a feed of 200+ posts).</p>
<p>Second, keep <code>PostCard</code> build methods cheap. Every rebuild of a postcard is expensive at scale. Use <code>const</code> constructors everywhere possible. Avoid rebuilding the whole card when only the like count changes. Isolate the like button into its own Riverpod consumer.</p>
<pre><code class="language-dart">// Bad — whole PostCard rebuilds when like changes
class PostCard extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final post = ref.watch(feedNotifierProvider)
        .valueOrNull
        ?.firstWhere((p) =&gt; p.id == postId);
    // ...
  }
}

// Good — only LikeButton rebuilds
class LikeButton extends ConsumerWidget {
  final String postId;
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final post = ref.watch(
      feedNotifierProvider.select(
        (state) =&gt; state.valueOrNull?.firstWhere((p) =&gt; p.id == postId),
      ),
    );
    // Only rebuilds when this specific post's like state changes
  }
}
</code></pre>
<p>Third, cache network images aggressively. Use <code>cached_network_image</code> with a memory cache limit. On a feed with avatars and post images, uncached network images are the single biggest source of jank.</p>
<p>And lastly, dispose WebSocket connections on screen exit. Don't keep a real-time connection alive when the user navigates away. Riverpod's <code>ref.onDispose</code> makes this straightforward, but it's easy to miss.</p>
<h2 id="heading-6-other-questions-to-prepare-for">6. Other Questions to Prepare For</h2>
<p>The social feed covers most of the hard architectural territory. These additional questions round out your preparation:</p>
<p><strong>Architecture &amp; structure:</strong></p>
<ul>
<li><p>How would you structure a large Flutter app for a team of 10 engineers?</p>
</li>
<li><p>How do you handle shared state between two features that shouldn't know about each other?</p>
</li>
<li><p>Walk me through how you'd design the data layer for an offline-first app.</p>
</li>
</ul>
<p><strong>State management:</strong></p>
<ul>
<li><p>Compare Riverpod, Bloc, and Redux from an architecture standpoint (not just API differences).</p>
</li>
<li><p>How do you prevent the state from leaking between sessions after a user logs out?</p>
</li>
</ul>
<p><strong>Networking &amp; data:</strong></p>
<ul>
<li><p>How would you handle token refresh across concurrent requests?</p>
</li>
<li><p>Walk me through optimistic UI for a financial transaction. How is it different from liking a post?</p>
</li>
</ul>
<p><strong>Performance:</strong></p>
<ul>
<li><p>A screen has 10,000 items. How do you render it without jank?</p>
</li>
<li><p>How do you design an image-loading system for a feed with mixed media types?</p>
</li>
</ul>
<p><strong>Multi-platform:</strong></p>
<ul>
<li><p>How would you share models and business logic between a Flutter mobile app and a Dart backend?</p>
</li>
<li><p>What changes about your architecture when you add a web as a target?</p>
</li>
</ul>
<p>For each of these, use the same framework: clarify the constraints, define the data model, name the layers, solve the hard problems explicitly, and address failure states.</p>
<h2 id="heading-7-key-takeaways">7. Key Takeaways</h2>
<p>Systems design is not a backend discipline that Flutter engineers are exempt from. It's a way of thinking about software that becomes unavoidable as apps grow in complexity, teams grow in size, and AI agents become part of the development workflow.</p>
<p>The social feed scenario illustrates five principles that apply across every frontend systems design problem:</p>
<h3 id="heading-1-layer-boundaries-are-load-bearing">1. Layer Boundaries Are Load-bearing</h3>
<p>The repository pattern, the separation of real-time from data fetching, and the isolation of pending actions aren't academic choices. They're what makes the system testable, navigable, and maintainable when requirements change.</p>
<h3 id="heading-2-the-data-model-anchors-everything">2. The Data Model Anchors Everything</h3>
<p>Decisions you make in the model (like cursor-based pagination, <code>isLikedByMe</code> on the post, and integer counts instead of arrays) ripple through every layer. Get the model right before designing anything else.</p>
<h3 id="heading-3-optimistic-ui-is-a-ux-contract-not-just-a-pattern">3. Optimistic UI is a UX Contract, Not Just a Pattern</h3>
<p>When you apply an optimistic update, you're making a promise to the user. Know when that promise is appropriate (social interactions) and when it isn't (financial transactions).</p>
<h3 id="heading-4-real-time-is-an-architecture-concern-not-a-feature">4. Real-time is an Architecture Concern, Not a Feature</h3>
<p>A WebSocket connection is a persistent resource that needs to be managed, connected when needed, disconnected when not, and reconnected on failure. Design it as infrastructure, not as part of a single screen.</p>
<h3 id="heading-5-offline-is-a-first-class-state">5. Offline is a First-class State</h3>
<p>Not an edge case, not a "nice to have." In markets with unreliable connectivity, which includes most of the world's fastest-growing mobile markets, an app that shows nothing when the network drops is a broken app.</p>
<p>The engineers who understand these principles and can articulate them out loud under interview pressure are the ones who get hired to build the systems that millions of people use.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ A Deep Dive into Behavioral Patterns: The Visitor Design Pattern and its Clean Operations Across Complex Object Structures ]]>
                </title>
                <description>
                    <![CDATA[ There's a problem that shows up in almost every growing software system, and most developers don't even realize they're hitting it until the damage is already done. You have a set of objects: differen ]]>
                </description>
                <link>https://www.freecodecamp.org/news/the-visitor-design-pattern-and-its-clean-operations-across-complex-object-structures/</link>
                <guid isPermaLink="false">6a74b21fcf90c22a668963b6</guid>
                
                    <category>
                        <![CDATA[ Design ]]>
                    </category>
                
                    <category>
                        <![CDATA[ design patterns ]]>
                    </category>
                
                    <category>
                        <![CDATA[ design principles ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Dart ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mobile ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Mobile Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Software Engineering ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software architecture ]]>
                    </category>
                
                    <category>
                        <![CDATA[ visitor design pattern ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Oluwaseyi Fatunmole ]]>
                </dc:creator>
                <pubDate>Thu, 06 Aug 2026 16:11:11 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/ff25cbd5-72fc-4f17-8d37-ba8dc909de46.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>There's a problem that shows up in almost every growing software system, and most developers don't even realize they're hitting it until the damage is already done.</p>
<p>You have a set of objects: different types, shapes, and data. And at some point, someone asks you to perform an operation on all of them, like exporting them them to PDF, sending them a notification, generating a report, or calculating their fees.</p>
<p>Your first instinct might be to write a function that checks the type and branches accordingly, like an if-else block or switch statement. Something that says: if this is a NewUser, do this. If this is a JointAccountUser, do that. It works, you ship it, and everyone is happy.</p>
<p>Then another operation comes in. And another. Every single time, you go back to the same place and add another branch. The function grows. The class grows. The test surface grows. What started as a clean model is now a god object that knows how to do everything for everyone.</p>
<p>The Visitor Design Pattern exists to break this cycle completely.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-is-the-visitor-design-pattern">What is the Visitor Design Pattern?</a></p>
</li>
<li><p><a href="#heading-the-problem-the-visitor-pattern-solves">The Problem the Visitor Pattern Solves</a></p>
</li>
<li><p><a href="#heading-core-components">Core Components</a></p>
</li>
<li><p><a href="#heading-real-world-example-one-document-export">Real World Example One: Document Export</a></p>
</li>
<li><p><a href="#heading-real-world-example-two-notification-system">Real World Example Two: Notification System</a></p>
</li>
<li><p><a href="#heading-real-world-example-three-fee-calculation">Real World Example Three: Fee Calculation</a></p>
</li>
<li><p><a href="#heading-the-power-of-combining-all-three-operations">The Power of Combining All Three Operations</a></p>
</li>
<li><p><a href="#heading-when-to-use-the-visitor-pattern">When to Use the Visitor Pattern</a></p>
</li>
<li><p><a href="#heading-when-not-to-use-it">When Not to Use It</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-the-visitor-design-pattern">What is the Visitor Design Pattern?</h2>
<p>The Visitor pattern is a behavioral design pattern that lets you define a new operation on a family of objects without changing the objects themselves.</p>
<p>The key word there is behavioral. Behavioral patterns are about how objects communicate and distribute responsibility. Where creational patterns deal with how objects are created and structural patterns deal with how they are composed, behavioral patterns deal with how they interact and who is responsible for what.</p>
<p>The Visitor pattern specifically deals with the question of who should own an operation when that operation needs to work differently across multiple object types.</p>
<p>The classic answer is: put the operation on each object. Give each class a method that handles the operation for its own type. But this breaks down the moment you have multiple operations, because now every new operation means touching every class. You're spreading one concern across your entire object hierarchy.</p>
<p>The Visitor pattern flips this. Instead of spreading the operation across the objects, you collect it into one place called a Visitor. The objects simply accept the visitor and let it do its work. Adding a new operation means creating a new Visitor. The existing objects don't change at all.</p>
<p>This is the Open/Closed Principle working exactly as intended: open for extension, closed for modification.</p>
<h2 id="heading-the-problem-the-visitor-pattern-solves">The Problem the Visitor Pattern Solves</h2>
<p>Let me show you exactly what this looks like without the Visitor pattern.</p>
<p>Say you have a fintech platform with four types of users: existing customers, new customers, minor account holders, and joint account holders. Your product manager comes in and asks you to add document export. Every user type should be exportable to PDF, Excel, and CSV.</p>
<p>Without Visitor, the natural approach looks something like this:</p>
<pre><code class="language-dart">class ExistingUser {
  final int id;
  final String firstName;
  final String lastName;
  final DateTime lastPaymentDate;
  final num accountBalance;

  String exportToPdf() {
    return '$firstName\n$lastName\n$lastPaymentDate\n$accountBalance';
  }

  String exportToExcel() {
    return '$firstName,$lastName,$lastPaymentDate,$accountBalance';
  }

  String exportToCsv() {
    return '"$firstName","$lastName","$lastPaymentDate","$accountBalance"';
  }
}
</code></pre>
<p>And you repeat this for NewUser, MinorAccountUser, and JointAccountUser. Twelve methods spread across four classes just for document export.</p>
<p>Now the product manager comes back. They want notifications: email, SMS, and Push. Back you go to all four classes, adding three more methods each. Twelve more methods spread across the same four classes.</p>
<p>Then they want fee calculation. Then they want KYC status checks. Every new operation multiplies across every user type. The classes grow, the reasons to change multiply, and testing becomes painful.</p>
<p>This is the exact problem the Visitor pattern was built to solve.</p>
<h2 id="heading-core-components">Core Components</h2>
<p>The Visitor pattern has four core components. Understanding each one before looking at code makes the implementation much easier to follow.</p>
<h3 id="heading-the-visitor-interface">The Visitor Interface</h3>
<p>This is the contract that every visitor must implement. It declares one method per object type it needs to visit. A visitor that handles four user types declares four visit methods, one for each type.</p>
<h3 id="heading-the-concrete-visitors">The Concrete Visitors</h3>
<p>These are the real implementations of the Visitor interface. Each one represents a single operation and knows how to handle every object type. A PdfHandler is a concrete visitor. An ExcelHandler is a concrete visitor. A SmsNotificationHandler is a concrete visitor. Each one has one job and knows how to do that job for every user type.</p>
<h3 id="heading-the-consumer-interface-also-called-element-or-acceptor">The Consumer Interface (also called Element or Acceptor)</h3>
<p>This is the contract that every object in the hierarchy must implement. It declares a single accept method that takes a Visitor and calls the right visit method on it. This is the double dispatch mechanism that makes the pattern work.</p>
<h3 id="heading-the-concrete-consumers">The Concrete Consumers</h3>
<p>These are the real objects in the hierarchy: ExistingCustomers, NewCustomers, MinorCustomer, and JointCustomer. Each one implements accept by calling the specific visit method that corresponds to its own type.</p>
<p>Think of it this way. The Visitor interface is implemented by every operation you want to perform: PdfHandler, ExcelHandler, and CsvHandler. Each of these knows how to handle all four user types.</p>
<p>The Consumer interface is implemented by every object in the hierarchy: ExistingCustomers, NewCustomers, MinorCustomer, and JointCustomer. Each of these knows how to receive a visitor and route it to the correct method.</p>
<p>When you call <code>existingCustomer.accept(pdfHandler)</code>, ExistingCustomers calls <code>pdfHandler.visitExistingCustomer(this)</code> and passes itself as the argument. The right method fires automatically. There's no type checking, if-else, or switch. The object tells the visitor who it is, and the visitor knows exactly what to do with that information.</p>
<h2 id="heading-real-world-example-one-document-export">Real World Example One: Document Export</h2>
<p>This is a real scenario from a fintech platform. There are four user types with different data structures, all needing to export their information to three document formats: PDF, Excel, and CSV.</p>
<h3 id="heading-step-1-define-the-user-models">Step 1: Define the User Models</h3>
<pre><code class="language-dart">class ExistingUser {
  final int id;
  final String firstName;
  final String lastName;
  final DateTime lastPaymentDate;
  final num accountBalance;

  const ExistingUser({
    required this.id,
    required this.firstName,
    required this.lastName,
    required this.lastPaymentDate,
    required this.accountBalance,
  });
}

class NewUser {
  final String firstName;
  final String lastName;

  const NewUser({
    required this.firstName,
    required this.lastName,
  });
}

class MinorAccountUser {
  final int age;
  final int guardianId;
  final String firstName;
  final String lastName;
  final String guardianName;

  const MinorAccountUser({
    required this.age,
    required this.guardianId,
    required this.firstName,
    required this.lastName,
    required this.guardianName,
  });
}

class JointAccountUser {
  final int jointAccountId;
  final List&lt;String&gt; accountHoldersInfo;
  final num accountBalance;

  const JointAccountUser({
    required this.jointAccountId,
    required this.accountHoldersInfo,
    required this.accountBalance,
  });
}
</code></pre>
<p>We have four models. Each one owns its own data and nothing else. There's no export logic or notification logic. And no business operations of any kind. Just clean data structures.</p>
<p>This is exactly how it should be. The model's job is to hold data. The visitor's job is to operate on it.</p>
<h3 id="heading-step-2-define-the-visitor-and-consumer-interfaces">Step 2: Define the Visitor and Consumer Interfaces</h3>
<pre><code class="language-dart">abstract class UserVisitor&lt;T&gt; {
  T visitExistingCustomer(ExistingUser user);
  T visitNewCustomer(NewUser user);
  T visitMinorCustomer(MinorAccountUser user);
  T visitJointCustomer(JointAccountUser user);
}

abstract class UserConsumer {
  T accept&lt;T&gt;(UserVisitor&lt;T&gt; visitor);
}
</code></pre>
<p><code>UserVisitor&lt;T&gt;</code> is generic. The type parameter <code>T</code> represents what the visitor returns. A document export visitor returns a String. A fee calculation visitor might return a double. A validation visitor might return a bool. The same pattern works for any return type.</p>
<p><code>UserConsumer</code> declares the accept method. Every object in the hierarchy must implement this. The accept method is what makes the double dispatch work. The object receives the visitor and immediately calls the right visit method on it, passing itself as the argument.</p>
<h3 id="heading-step-3-implement-the-concrete-consumers">Step 3: Implement the Concrete Consumers</h3>
<pre><code class="language-dart">class ExistingCustomers implements UserConsumer {
  final ExistingUser user;
  ExistingCustomers({required this.user});

  @override
  T accept&lt;T&gt;(UserVisitor&lt;T&gt; visitor) {
    return visitor.visitExistingCustomer(user);
  }
}

class NewCustomers implements UserConsumer {
  final NewUser user;
  NewCustomers({required this.user});

  @override
  T accept&lt;T&gt;(UserVisitor&lt;T&gt; visitor) {
    return visitor.visitNewCustomer(user);
  }
}

class MinorCustomer implements UserConsumer {
  final MinorAccountUser user;
  MinorCustomer({required this.user});

  @override
  T accept&lt;T&gt;(UserVisitor&lt;T&gt; visitor) {
    return visitor.visitMinorCustomer(user);
  }
}

class JointCustomer implements UserConsumer {
  final JointAccountUser user;
  JointCustomer({required this.user});

  @override
  T accept&lt;T&gt;(UserVisitor&lt;T&gt; visitor) {
    return visitor.visitJointCustomer(user);
  }
}
</code></pre>
<p>Each consumer wraps one user model and implements accept by forwarding to the correct visit method. This is the entire job of a concrete consumer. It knows who it is, and it tells the visitor by calling the right method.</p>
<p>Notice that none of these classes know anything about PDF, Excel, CSV, email, SMS, or any operation. They're completely decoupled from every operation that will ever be performed on them.</p>
<h3 id="heading-step-4-implement-the-concrete-visitors">Step 4: Implement the Concrete Visitors</h3>
<pre><code class="language-dart">class PdfHandler implements UserVisitor&lt;String&gt; {
  @override
  String visitExistingCustomer(ExistingUser user) {
    return '${user.firstName} ${user.lastName}'
        '\nBalance: ${user.accountBalance}'
        '\nLast Payment: ${user.lastPaymentDate}';
  }

  @override
  String visitNewCustomer(NewUser user) {
    return '${user.firstName} ${user.lastName}';
  }

  @override
  String visitMinorCustomer(MinorAccountUser user) {
    return '${user.firstName} ${user.lastName}'
        '\nAge: ${user.age}'
        '\nGuardian: ${user.guardianName} (ID: ${user.guardianId})';
  }

  @override
  String visitJointCustomer(JointAccountUser user) {
    final holders = user.accountHoldersInfo.join(', ');
    return 'Joint Account ID: ${user.jointAccountId}'
        '\nHolders: $holders'
        '\nBalance: ${user.accountBalance}';
  }
}

class ExcelHandler implements UserVisitor&lt;String&gt; {
  @override
  String visitExistingCustomer(ExistingUser user) {
    return '${user.firstName}\t${user.lastName}'
        '\t${user.accountBalance}\t${user.lastPaymentDate}';
  }

  @override
  String visitNewCustomer(NewUser user) {
    return '${user.firstName}\t${user.lastName}';
  }

  @override
  String visitMinorCustomer(MinorAccountUser user) {
    return '${user.firstName}\t${user.lastName}'
        '\t${user.age}\t${user.guardianName}\t${user.guardianId}';
  }

  @override
  String visitJointCustomer(JointAccountUser user) {
    final holders = user.accountHoldersInfo.join('\t');
    return '${user.jointAccountId}\t$holders\t${user.accountBalance}';
  }
}

class CsvHandler implements UserVisitor&lt;String&gt; {
  @override
  String visitExistingCustomer(ExistingUser user) {
    return '"${user.firstName}","${user.lastName}"'
        ',"${user.accountBalance}","${user.lastPaymentDate}"';
  }

  @override
  String visitNewCustomer(NewUser user) {
    return '"${user.firstName}","${user.lastName}"';
  }

  @override
  String visitMinorCustomer(MinorAccountUser user) {
    return '"${user.firstName}","${user.lastName}"'
        ',"${user.age}","${user.guardianName}","${user.guardianId}"';
  }

  @override
  String visitJointCustomer(JointAccountUser user) {
    final holders = user.accountHoldersInfo.map((h) =&gt; '"$h"').join(',');
    return '"${user.jointAccountId}",$holders,"${user.accountBalance}"';
  }
}
</code></pre>
<p>Each handler implements the visitor interface and knows exactly how to format each user type for its specific document format. PdfHandler uses newlines and labels. ExcelHandler uses tabs. CsvHandler wraps values in quotes and separates with commas.</p>
<p>The formatting logic for each document type lives in exactly one class. If the PDF format changes, you touch only PdfHandler. If the CSV format changes, you touch only CsvHandler. The user models never change.</p>
<h3 id="heading-step-5-use-it">Step 5: Use It</h3>
<pre><code class="language-dart">void existingUserLogic() {
  final customer = ExistingCustomers(
    user: ExistingUser(
      id: 10,
      firstName: 'Oluwaseyi',
      lastName: 'Fatunmole',
      lastPaymentDate: DateTime.now(),
      accountBalance: 7373773.39,
    ),
  );

  final pdf = customer.accept(PdfHandler());
  final excel = customer.accept(ExcelHandler());
  final csv = customer.accept(CsvHandler());

  print('PDF:\n$pdf\n');
  print('Excel:\n$excel\n');
  print('CSV:\n$csv\n');
}

void newUserLogic() {
  final customer = NewCustomers(
    user: NewUser(
      firstName: 'Oluwaseyi',
      lastName: 'Fatunmole',
    ),
  );

  customer.accept(PdfHandler());
  customer.accept(ExcelHandler());
  customer.accept(CsvHandler());
}

void minorUserLogic() {
  final customer = MinorCustomer(
    user: MinorAccountUser(
      age: 15,
      guardianId: 82882,
      firstName: 'Oluwaseyi',
      lastName: 'Fatunmole',
      guardianName: 'Inioluwa',
    ),
  );

  customer.accept(PdfHandler());
  customer.accept(ExcelHandler());
  customer.accept(CsvHandler());
}

void jointUserLogic() {
  final customer = JointCustomer(
    user: JointAccountUser(
      jointAccountId: 92,
      accountHoldersInfo: [
        'Oluwaseyi',
        'Aderonke',
        'Inioluwa',
        'Tiwaloluwa',
      ],
      accountBalance: 9200020202.22,
    ),
  );

  customer.accept(PdfHandler());
  customer.accept(ExcelHandler());
  customer.accept(CsvHandler());
}
</code></pre>
<p>The same customer object accepts any visitor with the same call. The type dispatch happens automatically through the accept method. There's no type checking anywhere in the calling code, and no if-else or switch. Just <code>customer.accept(handler)</code> and the right method fires.</p>
<p>Now think about what happens when you need to add an XML export. You create one new class, XmlHandler, implement the four visit methods, and that's it. You don't touch ExistingUser, NewUser, MinorAccountUser, JointAccountUser, or any of the existing handlers. The system is genuinely open for extension and closed for modification.</p>
<h2 id="heading-real-world-example-two-notification-system">Real World Example Two: Notification System</h2>
<p>Here we have the same four user types and the same pattern. But it's a different operation entirely.</p>
<p>Your platform needs to notify users about account events. But not every user type should be notified the same way.</p>
<p>Existing users get email and push notifications. New users only get email because they haven't fully set up their profile yet. Minor account users get SMS to their guardian's number. Joint account users get notified on all channels because multiple people share the account.</p>
<p>Without the Visitor pattern, this logic would spread across all four user models or collapse into one enormous function full of type checks. With Visitor, it lives in three focused classes.</p>
<h3 id="heading-the-notification-visitor-interface">The Notification Visitor Interface</h3>
<pre><code class="language-dart">abstract class NotificationVisitor {
  void visitExistingCustomer(ExistingUser user);
  void visitNewCustomer(NewUser user);
  void visitMinorCustomer(MinorAccountUser user);
  void visitJointCustomer(JointAccountUser user);
}
</code></pre>
<p>This visitor returns void because notifications are side effects. They send messages, they don't return values.</p>
<h3 id="heading-the-concrete-notification-visitors">The Concrete Notification Visitors</h3>
<pre><code class="language-dart">class EmailNotificationHandler implements NotificationVisitor {
  @override
  void visitExistingCustomer(ExistingUser user) {
    print('Sending email to existing customer: ${user.firstName}');
    // email service call with full account details
  }

  @override
  void visitNewCustomer(NewUser user) {
    print('Sending welcome email to new customer: ${user.firstName}');
    // welcome email with onboarding steps
  }

  @override
  void visitMinorCustomer(MinorAccountUser user) {
    print('Sending email to guardian: ${user.guardianName}');
    // email goes to guardian, not the minor
  }

  @override
  void visitJointCustomer(JointAccountUser user) {
    for (final holder in user.accountHoldersInfo) {
      print('Sending email to joint holder: $holder');
      // all account holders get notified
    }
  }
}

class SmsNotificationHandler implements NotificationVisitor {
  @override
  void visitExistingCustomer(ExistingUser user) {
    print('Sending SMS to existing customer: ${user.firstName}');
  }

  @override
  void visitNewCustomer(NewUser user) {
    // new users are not SMS-verified yet, skip
    print('New customer ${user.firstName} not SMS-eligible yet');
  }

  @override
  void visitMinorCustomer(MinorAccountUser user) {
    print('Sending SMS to guardian ${user.guardianName} for minor ${user.firstName}');
    // SMS goes to guardian's registered number
  }

  @override
  void visitJointCustomer(JointAccountUser user) {
    for (final holder in user.accountHoldersInfo) {
      print('Sending SMS to joint holder: $holder');
    }
  }
}

class PushNotificationHandler implements NotificationVisitor {
  @override
  void visitExistingCustomer(ExistingUser user) {
    print('Push notification to existing customer: ${user.firstName}');
  }

  @override
  void visitNewCustomer(NewUser user) {
    print('Push notification to new customer: ${user.firstName}');
  }

  @override
  void visitMinorCustomer(MinorAccountUser user) {
    // minors do not have the app installed yet, guardian gets push
    print('Push notification to guardian: ${user.guardianName}');
  }

  @override
  void visitJointCustomer(JointAccountUser user) {
    for (final holder in user.accountHoldersInfo) {
      print('Push notification to joint holder: $holder');
    }
  }
}
</code></pre>
<p>Each handler knows the specific rules for each user type. <code>SmsNotificationHandler</code> knows that new users aren't SMS-verified yet. <code>PushNotificationHandler</code> knows that minor account notifications go to the guardian. <code>EmailNotificationHandler</code> knows that joint account holders all need to be notified individually.</p>
<p>This business logic lives in exactly one place per notification channel. When the rules change (and they always change), you update one class.</p>
<h3 id="heading-using-the-notification-visitors">Using the Notification Visitors</h3>
<pre><code class="language-dart">void notifyExistingUser() {
  final customer = ExistingCustomers(
    user: ExistingUser(
      id: 10,
      firstName: 'Oluwaseyi',
      lastName: 'Fatunmole',
      lastPaymentDate: DateTime.now(),
      accountBalance: 7373773.39,
    ),
  );

  customer.accept(EmailNotificationHandler());
  customer.accept(SmsNotificationHandler());
  customer.accept(PushNotificationHandler());
}

void notifyMinorUser() {
  final customer = MinorCustomer(
    user: MinorAccountUser(
      age: 15,
      guardianId: 82882,
      firstName: 'Oluwaseyi',
      lastName: 'Fatunmole',
      guardianName: 'Inioluwa',
    ),
  );

  // all three channels fire, each with minor-specific rules
  customer.accept(EmailNotificationHandler());
  customer.accept(SmsNotificationHandler());
  customer.accept(PushNotificationHandler());
}

void notifyJointUser() {
  final customer = JointCustomer(
    user: JointAccountUser(
      jointAccountId: 92,
      accountHoldersInfo: [
        'Oluwaseyi',
        'Aderonke',
        'Inioluwa',
        'Tiwaloluwa',
      ],
      accountBalance: 9200020202.22,
    ),
  );

  customer.accept(EmailNotificationHandler());
  customer.accept(SmsNotificationHandler());
  customer.accept(PushNotificationHandler());
}
</code></pre>
<p>The calling code is identical regardless of the user type or the notification channel. The dispatch is automatic. The rules live inside the visitors.</p>
<p>When WhatsApp notifications become a requirement (and they will), you create one <code>WhatsAppNotificationHandler</code> class with four visit methods. Nothing else changes.</p>
<h2 id="heading-real-world-example-three-fee-calculation">Real World Example Three: Fee Calculation</h2>
<p>Again, we have the same four user types and the same pattern. And once again, we have a completely different operation.</p>
<p>Your platform needs to calculate monthly maintenance fees. But each user type has different rules.</p>
<p>Existing customers pay a flat monthly fee based on their account balance. New customers are fee-exempt for their first three months. Minor account holders pay a reduced fee because their accounts have restricted features. Joint account holders have their fee split equally across all account holders.</p>
<p>Without Visitor, this logic ends up as a giant method somewhere with four branches, or worse, it leaks into the user models themselves. With Visitor, it lives in one focused class.</p>
<h3 id="heading-the-fee-visitor-interface">The Fee Visitor Interface</h3>
<pre><code class="language-dart">abstract class FeeVisitor {
  double visitExistingCustomer(ExistingUser user);
  double visitNewCustomer(NewUser user);
  double visitMinorCustomer(MinorAccountUser user);
  double visitJointCustomer(JointAccountUser user);
}
</code></pre>
<p>This visitor returns a double because fee calculation produces a numeric value.</p>
<h3 id="heading-the-concrete-fee-visitor">The Concrete Fee Visitor</h3>
<pre><code class="language-dart">class MonthlyFeeCalculator implements FeeVisitor {
  @override
  double visitExistingCustomer(ExistingUser user) {
    // 0.5% of account balance, minimum 500, maximum 5000
    final fee = user.accountBalance * 0.005;
    return fee.clamp(500, 5000).toDouble();
  }

  @override
  double visitNewCustomer(NewUser user) {
    // new customers are fee-exempt for the first 3 months
    return 0.0;
  }

  @override
  double visitMinorCustomer(MinorAccountUser user) {
    // flat reduced fee for minor accounts
    return 150.0;
  }

  @override
  double visitJointCustomer(JointAccountUser user) {
    // standard fee split equally across all holders
    const standardFee = 2000.0;
    return standardFee / user.accountHoldersInfo.length;
  }
}
</code></pre>
<p>Every fee rule for every user type lives in this one class. When the fee structure changes for existing customers, you touch one method in one class. When minor account fees are updated, same thing. None of the user models change, and no other visitor changes.</p>
<h3 id="heading-using-the-fee-visitor">Using the Fee Visitor</h3>
<pre><code class="language-dart">void calculateFees() {
  final existingCustomer = ExistingCustomers(
    user: ExistingUser(
      id: 10,
      firstName: 'Oluwaseyi',
      lastName: 'Fatunmole',
      lastPaymentDate: DateTime.now(),
      accountBalance: 7373773.39,
    ),
  );

  final newCustomer = NewCustomers(
    user: NewUser(
      firstName: 'Aderonke',
      lastName: 'Fatunmole',
    ),
  );

  final minorCustomer = MinorCustomer(
    user: MinorAccountUser(
      age: 15,
      guardianId: 82882,
      firstName: 'Inioluwa',
      lastName: 'Fatunmole',
      guardianName: 'Oluwaseyi',
    ),
  );

  final jointCustomer = JointCustomer(
    user: JointAccountUser(
      jointAccountId: 92,
      accountHoldersInfo: [
        'Oluwaseyi',
        'Aderonke',
        'Inioluwa',
        'Tiwaloluwa',
      ],
      accountBalance: 9200020202.22,
    ),
  );

  final calculator = MonthlyFeeCalculator();

  final existingFee = existingCustomer.accept(calculator);
  final newFee = newCustomer.accept(calculator);
  final minorFee = minorCustomer.accept(calculator);
  final jointFee = jointCustomer.accept(calculator);

  print('Existing customer fee: NGN $existingFee');
  print('New customer fee: NGN $newFee');
  print('Minor account fee: NGN $minorFee');
  print('Joint account fee per holder: NGN $jointFee');
}
</code></pre>
<p>The output:</p>
<pre><code class="language-plaintext">Existing customer fee: NGN 5000.0
New customer fee: NGN 0.0
Minor account fee: NGN 150.0
Joint account fee per holder: NGN 500.0
</code></pre>
<p>When a <code>PremiumFeeCalculator</code> is needed for a new tier of customers, you create one new class that implements <code>FeeVisitor</code>. The user models stay exactly as they are. The <code>MonthlyFeeCalculator</code> stays exactly as it is. The accept methods on all four consumers stay exactly as they are.</p>
<h2 id="heading-the-power-of-combining-all-three-operations">The Power of Combining All Three Operations</h2>
<p>Here's what makes the Visitor pattern truly shine in a system like this. You have the same four user types, and you can run any combination of visitors on any of them in the same call chain.</p>
<pre><code class="language-dart">void processUser(UserConsumer customer) {
  final pdf = customer.accept(PdfHandler());
  final csv = customer.accept(CsvHandler());

  customer.accept(EmailNotificationHandler());
  customer.accept(PushNotificationHandler());

  final fee = customer.accept(MonthlyFeeCalculator());

  print('Fee: NGN $fee');
  print('Documents generated and notifications sent');
}
</code></pre>
<p>One function, any user type, any combination of operations. The consumer doesn't care which visitors it receives. The visitors don't care which consumers call them. They speak to each other through the interface, and the interface guarantees everything works correctly.</p>
<p>We have three completely different operations (document export, notifications, and fee calculation) all applied to the same object with the same call pattern. None of these operations know about each other. None of them touch the user models. Each one lives in its own focused class with its own single reason to change.</p>
<h2 id="heading-when-to-use-the-visitor-pattern">When to Use the Visitor Pattern</h2>
<p>Use Visitor when you have a stable set of object types and a growing set of operations on them.</p>
<p>The pattern shines when the object hierarchy is unlikely to change frequently. It's optimized for adding new operations, not new types. Adding a new user type means updating every existing visitor. If your object types change constantly, Visitor creates more work than it saves.</p>
<p>It's also very effective when you need to perform multiple unrelated operations on a family of objects without polluting their classes with that logic. Document export, notification handling, fee calculation, and KYC validation are all unrelated operations. Each belongs in its own visitor, not scattered across the user models.</p>
<p>Visitor also works well when you want clean separation between data and behavior. The models hold data and the visitors define behavior. This makes both easier to understand, easier to test, and easier to maintain independently.</p>
<h2 id="heading-when-not-to-use-it">When Not to Use It</h2>
<p>Avoid Visitor when the object hierarchy changes frequently. Every time you add a new type, you must update every existing visitor. In a system where new user types appear regularly, this becomes painful quickly.</p>
<p>It's also not helpful when you only have one or two operations. For simple cases, the overhead of creating visitor interfaces, consumer interfaces, and multiple classes is not worth the benefit.</p>
<p>And avoid it when the operations are tightly coupled to the object's internal state in ways that make sense to keep together. Some behavior naturally belongs on the object itself.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>The Visitor Design Pattern solves a problem that most developers only recognize after they've already made a mess of it. You have a family of objects with different types and different data. Operations come in one after another. Without a deliberate structure, those operations spread everywhere: into the models, utility classes, and massive switch statements that nobody wants to touch.</p>
<p>Visitor collects each operation into one focused class. The models stay clean and the operations stay isolated. Adding a new operation means creating one new class. The existing code doesn't change.</p>
<p>In the fintech examples above, we have three entirely different concerns: document export, notifications, and fee calculation. All are handled by handled by focused classes, none of which know anything about each other. The user models don't know about PDF or email or fees. The PdfHandler doesn't know about SMS. The MonthlyFeeCalculator doesn't know about push notifications. Each class has exactly one reason to exist and exactly one reason to change.</p>
<p>That s what a well-applied Visitor pattern looks like in practice. Clean, focused, and genuinely extensible.</p>
<p>Happy coding!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Structure Large Flutter Applications for Scalable and Maintainable Growth ]]>
                </title>
                <description>
                    <![CDATA[ Flutter makes it extremely fast to build UIs. That speed is one of the framework’s greatest strengths, but it also creates a subtle problem: applications often grow much faster than their architecture ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-structure-large-flutter-applications-for-scalable-and-maintainable-growth/</link>
                <guid isPermaLink="false">6a3ab6b8b961d002e47ff767</guid>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Mobile Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software architecture ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Ethiel ADIASSA ]]>
                </dc:creator>
                <pubDate>Tue, 23 Jun 2026 16:39:20 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/6196a6e1-d542-40f3-9be1-c303b8d6aace.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Flutter makes it extremely fast to build UIs. That speed is one of the framework’s greatest strengths, but it also creates a subtle problem: applications often grow much faster than their architecture.</p>
<p>A few screens quickly become dozens. Features that initially felt isolated start interacting with each other. Authentication affects navigation. Notifications affect onboarding. Feature flags alter business flows. Local persistence introduces synchronization concerns. State begins leaking between unrelated parts of the application.</p>
<p>None of this happens suddenly.</p>
<p>Most Flutter codebases degrade progressively. Small shortcuts that felt harmless early on accumulate until changing one feature requires understanding half the application.</p>
<p>This is usually where teams begin introducing architecture patterns reactively. Unfortunately, many applications attempt to solve scaling problems by adding abstraction layers without first understanding where the actual complexity comes from.</p>
<p>Large applications rarely fail because they lack patterns. They fail because ownership boundaries become unclear.</p>
<p>This article presents a practical approach to structuring large Flutter applications so complexity remains visible and manageable as the codebase evolves. The focus here isn't theoretical purity. It's long-term maintainability under real production constraints.</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-what-makes-flutter-apps-hard-to-scale">What Makes Flutter Apps Hard to Scale</a></p>
</li>
<li><p><a href="#heading-why-small-architectures-break-down">Why Small Architectures Break Down</a></p>
</li>
<li><p><a href="#heading-organizing-by-feature">Organizing by Feature</a></p>
</li>
<li><p><a href="#heading-separating-presentation-domain-and-data">Separating Presentation, Domain, and Data</a></p>
</li>
<li><p><a href="#heading-state-boundaries-and-state-management">State Boundaries and State Management</a></p>
</li>
<li><p><a href="#heading-navigation-at-scale">Navigation at Scale</a></p>
</li>
<li><p><a href="#heading-managing-shared-code">Managing Shared Code</a></p>
</li>
<li><p><a href="#heading-scaling-dependency-injection">Scaling Dependency Injection</a></p>
</li>
<li><p><a href="#heading-production-considerations">Production Considerations</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>This guide assumes familiarity with Flutter widgets, asynchronous programming with <code>Future</code> and <code>async/await</code>, and basic state management approaches such as Provider, Riverpod, or BLoC.</p>
<p>You should also already feel comfortable building applications beyond simple demos. The article focuses less on Flutter fundamentals and more on architectural decisions that emerge once applications become long-lived systems maintained by multiple developers over time.</p>
<h2 id="heading-what-makes-flutter-apps-hard-to-scale">What Makes Flutter Apps Hard to Scale</h2>
<p>Large applications are rarely difficult because of UI complexity alone. Most scaling problems emerge from coordination complexity.</p>
<p>A simple login flow illustrates this well. Initially, authentication may only involve sending credentials, receiving a token, and navigating to a home screen.</p>
<p>But production systems evolve quickly. Authentication eventually becomes responsible for:</p>
<ul>
<li><p>restoring sessions</p>
</li>
<li><p>refreshing expired tokens</p>
</li>
<li><p>preloading user data</p>
</li>
<li><p>triggering analytics</p>
</li>
<li><p>handling onboarding state</p>
</li>
<li><p>synchronizing local caches</p>
</li>
<li><p>applying feature flags</p>
</li>
<li><p>supporting deep links</p>
</li>
</ul>
<p>The UI may still appear simple while the underlying coordination logic becomes increasingly interconnected.</p>
<p>Without architectural boundaries, this complexity spreads everywhere:</p>
<ul>
<li><p>widgets</p>
</li>
<li><p>repositories</p>
</li>
<li><p>route guards</p>
</li>
<li><p>interceptors</p>
</li>
<li><p>global services</p>
</li>
<li><p>state containers</p>
</li>
</ul>
<p>At that point, even small changes become risky because unrelated systems begin sharing lifecycle assumptions.</p>
<p>This is one of the most important architectural realities in Flutter applications: complexity scales through interactions, not screens.</p>
<h2 id="heading-why-small-architectures-break-down">Why Small Architectures Break Down</h2>
<p>Many Flutter applications begin with a structure like this:</p>
<pre><code class="language-text">lib/
  screens/
  widgets/
  services/
  providers/
  models/
</code></pre>
<p>For small applications, this works perfectly well. The problem appears once features become larger and more interconnected.</p>
<p>Imagine implementing a “favorites” feature. The screen lives in <code>screens/</code>. State management lives in <code>providers/</code>. Networking logic lives in <code>services/</code>. Models live in <code>models/</code>.</p>
<p>A single business capability now spans the entire project structure.</p>
<p>This introduces a subtle but important problem: the application structure no longer reflects the product structure.</p>
<p>Developers stop thinking in terms of features and start thinking in terms of technical categories.</p>
<p>Over time, ownership becomes ambiguous, dependencies become implicit, unrelated features become coupled, and debugging requires jumping constantly across folders.</p>
<p>The architecture begins optimizing for file classification instead of system comprehension.</p>
<p>That distinction matters more than it initially appears.</p>
<p>Large systems survive through clarity of ownership. Once ownership boundaries become blurry, maintenance costs rise aggressively.</p>
<h2 id="heading-organizing-by-feature">Organizing by Feature</h2>
<p>The most effective way to reduce architectural fragmentation is organizing the application around business capabilities instead of technical layers.</p>
<p>A feature should own everything required for its behavior:</p>
<ul>
<li><p>presentation</p>
</li>
<li><p>business logic</p>
</li>
<li><p>state</p>
</li>
<li><p>persistence</p>
</li>
<li><p>tests</p>
</li>
</ul>
<p>For example:</p>
<pre><code class="language-text">lib/
  features/
    authentication/
      presentation/
      domain/
      data/
</code></pre>
<p>As the feature evolves, its structure can grow naturally:</p>
<pre><code class="language-text">features/
  authentication/
    presentation/
      pages/
      widgets/
      state/
    domain/
      entities/
      usecases/
      repositories/
    data/
      models/
      repositories/
      sources/
</code></pre>
<p>Now the authentication system exists as a coherent unit instead of being scattered across the codebase.</p>
<p>This dramatically improves locality of change.</p>
<p>When developers modify authentication behavior, they immediately know where state lives, where business rules are defined, how persistence is implemented, and where tests belong.</p>
<p>This becomes increasingly important as multiple developers work simultaneously on unrelated features. Clear ownership boundaries reduce accidental coupling and make parallel development significantly safer.</p>
<p>The presentation layer reacts to state changes:</p>
<pre><code class="language-dart">class LoginPage extends StatelessWidget {
  const LoginPage({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocConsumer&lt;LoginCubit, LoginState&gt;(
      listener: (context, state) {
        if (state.isSuccess) {
          context.go('/home');
        }
      },
      builder: (context, state) {
        return LoginView(
          isLoading: state.isLoading,
          onSubmit: (email, password) {
            context.read&lt;LoginCubit&gt;().login(
              email,
              password,
            );
          },
        );
      },
    );
  }
}
</code></pre>
<p>The important detail here is not BLoC itself. It's the separation of responsibilities.</p>
<p>The widget renders UI and forwards user intent. It doesn't coordinate infrastructure concerns directly.</p>
<p>That orchestration happens elsewhere:</p>
<pre><code class="language-dart">class LoginCubit extends Cubit&lt;LoginState&gt; {
  final LoginUseCase loginUseCase;

  LoginCubit(this.loginUseCase)
      : super(const LoginState.initial());

  Future&lt;void&gt; login(
    String email,
    String password,
  ) async {
    emit(state.loading());

    final result = await loginUseCase(
      email,
      password,
    );

    result.fold(
      (failure) =&gt; emit(
        state.failure(failure.message),
      ),
      (_) =&gt; emit(
        state.success(),
      ),
    );
  }
}
</code></pre>
<p>This distinction prevents UI code from slowly becoming an orchestration layer filled with side effects.</p>
<h2 id="heading-separating-presentation-domain-and-data">Separating Presentation, Domain, and Data</h2>
<p>One of the most important architectural boundaries in large Flutter applications is separating presentation, business logic, and infrastructure concerns.</p>
<p>These layers evolve at different speeds: the UI changes constantly, while business rules evolve more slowly and infrastructure changes unpredictably.</p>
<p>Without separation, infrastructure concerns gradually leak upward into presentation code until widgets become tightly coupled to APIs, databases, caching, retries, and persistence logic.</p>
<p>A common anti-pattern looks like this:</p>
<pre><code class="language-dart">ElevatedButton(
  onPressed: () async {
    final response = await dio.post(
      '/login',
      data: {
        'email': email,
        'password': password,
      },
    );

    if (response.statusCode == 200) {
      Navigator.pushNamed(
        context,
        '/home',
      );
    }
  },
)
</code></pre>
<p>This may seem harmless initially, but it tightly couples networking, navigation, side effects, and widget lifecycle management.</p>
<p>The widget now owns infrastructure coordination. That becomes increasingly difficult to maintain as flows grow more complex.</p>
<p>Instead, the widget should simply emit user intent:</p>
<pre><code class="language-dart">ElevatedButton(
  onPressed: () {
    context.read&lt;LoginCubit&gt;().login(
      email,
      password,
    );
  },
)
</code></pre>
<p>The orchestration belongs in the application layer.</p>
<p>The domain layer contains business rules and repository contracts:</p>
<pre><code class="language-dart">abstract class AuthenticationRepository {
  Future&lt;User&gt; login(
    String email,
    String password,
  );
}
</code></pre>
<p>Use cases coordinate business behavior independently from infrastructure details:</p>
<pre><code class="language-dart">class LoginUseCase {
  final AuthenticationRepository repository;

  LoginUseCase(this.repository);

  Future&lt;User&gt; call(
    String email,
    String password,
  ) {
    return repository.login(
      email,
      password,
    );
  }
}
</code></pre>
<p>This separation matters because business rules shouldn't depend directly on HTTP clients, databases, or serialization details.</p>
<p>Infrastructure belongs in the data layer:</p>
<pre><code class="language-dart">class AuthenticationApi {
  final Dio dio;

  AuthenticationApi(this.dio);

  Future&lt;UserDto&gt; login(
    String email,
    String password,
  ) async {
    final response = await dio.post(
      '/login',
      data: {
        'email': email,
        'password': password,
      },
    );

    return UserDto.fromJson(
      response.data,
    );
  }
}
</code></pre>
<p>Repository implementations coordinate infrastructure concerns while keeping those details isolated from the rest of the system:</p>
<pre><code class="language-dart">class AuthenticationRepositoryImpl
    implements AuthenticationRepository {
  final AuthenticationApi api;

  AuthenticationRepositoryImpl(this.api);

  @override
  Future&lt;User&gt; login(
    String email,
    String password,
  ) async {
    final dto = await api.login(
      email,
      password,
    );

    return dto.toDomain();
  }
}
</code></pre>
<p>This architecture introduces more structure, but it also creates clearer ownership boundaries and safer system evolution over time. Furthermore the implementation details are encapsulated behind the interface. This practice facilitates testing and dependency injection.</p>
<h2 id="heading-state-boundaries-and-state-management">State Boundaries and State Management</h2>
<p>Most Flutter state management discussions focus heavily on libraries.</p>
<p>In practice, scaling problems usually come from ownership boundaries rather than tooling.</p>
<p>The hardest questions are rarely should we use Riverpod? Or should we use BLoC?</p>
<p>The harder questions are who owns this state and how long should it live? Who can mutate it? What systems depend on it? And what rebuild boundaries exist?</p>
<p>Many applications eventually accumulate giant global state containers:</p>
<pre><code class="language-dart">class AppBloc extends Bloc&lt;AppEvent, AppState&gt; {
  // authentication
  // profile
  // notifications
  // settings
  // analytics
}
</code></pre>
<p>Initially, this feels convenient because everything becomes accessible globally.</p>
<p>Over time, unrelated concerns begin sharing lifecycle assumptions. Features become tightly coupled through shared state. Rebuild propagation becomes harder to reason about. Debugging state transitions becomes increasingly expensive.</p>
<p>Instead, prefer feature-level ownership:</p>
<pre><code class="language-text">features/
  profile/
    state/
  checkout/
    state/
  notifications/
    state/
</code></pre>
<p>Each feature owns its own lifecycle and transitions.</p>
<p>For example:</p>
<pre><code class="language-dart">class CartCubit extends Cubit&lt;CartState&gt; {
  CartCubit()
      : super(
          const CartState.empty(),
        );

  void addProduct(Product product) {
    emit(
      state.copyWith(
        products: [
          ...state.products,
          product,
        ],
      ),
    );
  }
}
</code></pre>
<p>This dramatically reduces hidden coupling.</p>
<p>Other features should interact through events, abstractions, or use cases – not direct mutation.</p>
<p>Global state should remain limited to concerns that are truly global and span across multiple features. For example:</p>
<ul>
<li><p>authentication</p>
</li>
<li><p>localization</p>
</li>
<li><p>theme</p>
</li>
<li><p>application session</p>
</li>
</ul>
<p>Everything else should stay scoped whenever possible.</p>
<h2 id="heading-navigation-at-scale">Navigation at Scale</h2>
<p>Navigation complexity grows much faster than most teams expect.</p>
<p>Initially, routing may feel trivial: push a screen, pop a screen, maybe protect a route.</p>
<p>But production applications introduce:</p>
<ul>
<li><p>onboarding flows</p>
</li>
<li><p>deep links</p>
</li>
<li><p>nested navigation</p>
</li>
<li><p>authentication guards</p>
</li>
<li><p>modal coordination</p>
</li>
<li><p>state restoration</p>
</li>
<li><p>multiple navigation entry points</p>
</li>
</ul>
<p>Navigation logic should remain isolated from business logic since this is really critical as the application grows and the developers need to focus on business logic. Decoupling navigation logic from the business one is a foundational architectural best practice.</p>
<p>Repositories should never know about routing:</p>
<pre><code class="language-dart">class AuthenticationRepository {
  Future&lt;void&gt; login() async {
    Navigator.pushNamed(
      context,
      '/home',
    );
  }
}
</code></pre>
<p>This code creates coupling between infrastructure and presentation concerns.</p>
<p>Instead, business logic should emit outcomes:</p>
<pre><code class="language-dart">sealed class LoginResult {}

class LoginSuccess extends LoginResult {}

class LoginFailure extends LoginResult {
  final String message;

  LoginFailure(this.message);
}
</code></pre>
<p>The presentation layer reacts to those outcomes:</p>
<pre><code class="language-dart">BlocListener&lt;LoginCubit, LoginState&gt;(
  listener: (context, state) {
    if (state.isSuccess) {
      context.go('/home');
    }
  },
  child: const LoginView(),
)
</code></pre>
<p>This keeps routing decisions inside the presentation layer where they belong.</p>
<p>It also simplifies testing, debugging, and navigation ownership.</p>
<h2 id="heading-managing-shared-code">Managing Shared Code</h2>
<p>Large applications inevitably accumulate shared code.</p>
<p>The danger is allowing folders like <code>shared/</code>, <code>common/</code>, or <code>core/</code> to become dumping grounds for unrelated logic.</p>
<p>Shared UI primitives are excellent reuse candidates:</p>
<pre><code class="language-text">shared/
  widgets/
    app_button.dart
    app_text_field.dart
  theme/
  spacing/
</code></pre>
<p>But feature-specific logic should remain inside feature boundaries.</p>
<p>This quickly becomes dangerous:</p>
<pre><code class="language-text">shared/
  auth_helpers.dart
  checkout_utils.dart
</code></pre>
<p>Once business logic enters shared layers, a few things happen:</p>
<ul>
<li><p>ownership becomes unclear</p>
</li>
<li><p>unrelated features become coupled</p>
</li>
<li><p>architectural boundaries begin dissolving</p>
</li>
</ul>
<p>Premature abstraction often creates more long-term maintenance cost than small duplication.</p>
<p>If two features may evolve differently later, duplication may actually preserve isolation more effectively than forced reuse.</p>
<p>Maintainability matters more than maximizing reuse percentages.</p>
<h2 id="heading-scaling-dependency-injection">Scaling Dependency Injection</h2>
<p>Dependency injection helps isolate infrastructure and improve testability, but uncontrolled DI can easily become hidden global state.</p>
<p>Constructor injection remains one of the clearest approaches:</p>
<pre><code class="language-dart">class ProfileCubit extends Cubit&lt;ProfileState&gt; {
  final LoadProfileUseCase loadProfile;

  ProfileCubit(this.loadProfile)
      : super(
          const ProfileState.initial(),
        );
}
</code></pre>
<p>Dependencies remain visible and explicit.</p>
<p>Feature-level registration also improves modularity:</p>
<pre><code class="language-dart">void registerAuthenticationModule() {
  getIt.registerLazySingleton&lt;
      AuthenticationRepository&gt;(
    () =&gt; AuthenticationRepositoryImpl(
      getIt(),
    ),
  );

  getIt.registerFactory(
    () =&gt; LoginCubit(
      getIt(),
    ),
  );
}
</code></pre>
<p>Avoid arbitrary service locator access deep inside widgets:</p>
<pre><code class="language-dart">getIt&lt;ApiClient&gt;()
</code></pre>
<p>Hidden dependencies make debugging significantly harder because ownership becomes invisible.</p>
<p>Dependency ownership should follow feature ownership whenever possible.</p>
<h2 id="heading-production-considerations">Production Considerations</h2>
<p>Many architecture discussions stop before operational concerns appear.</p>
<p>Production systems introduce constraints that heavily influence architectural decisions, like:</p>
<ul>
<li><p>startup performance</p>
</li>
<li><p>observability</p>
</li>
<li><p>rollout safety</p>
</li>
<li><p>migration complexity</p>
</li>
<li><p>debugging visibility</p>
</li>
<li><p>operational consistency</p>
</li>
</ul>
<p>Avoid heavy synchronous initialization inside <code>main()</code>:</p>
<pre><code class="language-dart">Future&lt;void&gt; main() async {
  WidgetsFlutterBinding
      .ensureInitialized();

  await configureDependencies();

  runApp(
    const App(),
  );
}
</code></pre>
<p>Lazy initialization improves startup performance and reduces blocking work during application launch.</p>
<p>Observability also becomes essential once applications scale:</p>
<pre><code class="language-dart">FlutterError.onError =
    FirebaseCrashlytics.instance
        .recordFlutterFatalError;
</code></pre>
<p>Without observability, debugging production issues becomes increasingly expensive because failures become difficult to reproduce locally.</p>
<p>Feature flags reduce deployment risk and support gradual rollouts:</p>
<pre><code class="language-dart">if (
  featureFlags.isEnabled(
    'new_checkout',
  )
) {
  return const NewCheckoutPage();
}

return const LegacyCheckoutPage();
</code></pre>
<p>As teams grow, operational consistency matters more and more.</p>
<p>Large applications require linting, formatting, automated tests, static analysis, and pull request validation.</p>
<p>Architecture alone can't preserve maintainability without engineering discipline surrounding the system itself.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Large Flutter applications succeed when teams optimize for locality of change, explicit ownership, isolated state boundaries, predictable data flow, and maintainable system evolution.</p>
<p>Good architecture doesn't eliminate complexity. It makes complexity understandable.</p>
<p>Organize around features, keep infrastructure isolated, avoid hidden dependencies, treat state ownership seriously, and be careful with shared abstractions.</p>
<p>Most importantly, evolve architecture incrementally.</p>
<p>The best architectures are rarely designed all at once. They emerge from continuously reducing friction as the application, team, and operational complexity evolve together.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Pure Headless vs. Hybrid Headless CMS: Choosing the Right Architecture for Enterprise Content Management ]]>
                </title>
                <description>
                    <![CDATA[ Enterprise organisations are under constant pressure to deliver content across websites, mobile applications, customer portals, digital kiosks, smart devices, and emerging digital channels. Customers  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/pure-headless-vs-hybrid-headless-cms-for-enterprise-content-management/</link>
                <guid isPermaLink="false">6a35795fac5ab8c96cba139b</guid>
                
                    <category>
                        <![CDATA[ headless cms ]]>
                    </category>
                
                    <category>
                        <![CDATA[ enterprise ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software architecture ]]>
                    </category>
                
                    <category>
                        <![CDATA[ cms ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Fri, 19 Jun 2026 17:16:15 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/7fc18f0e-8543-415d-a675-6944bab57dcf.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Enterprise organisations are under constant pressure to deliver content across websites, mobile applications, customer portals, digital kiosks, smart devices, and emerging digital channels.</p>
<p>Customers expect consistent experiences wherever they interact with a brand, while internal teams need tools that simplify publishing, governance, localisation, and content operations.</p>
<p>As a result, many organisations are reevaluating their content management systems and moving away from traditional monolithic platforms. The rise of headless content management systems has introduced new possibilities for flexibility, scalability, and omnichannel content delivery.</p>
<p>But choosing between a Pure Headless CMS and a Hybrid Headless CMS isn't always straightforward. Both architectures support modern digital experiences, but they differ significantly in how they manage content, presentation layers, workflows, and enterprise requirements.</p>
<p>In this article, we'll explore the differences between Pure Headless CMS and Hybrid Headless CMS architectures, examine their strengths and limitations, and help enterprise decision-makers determine which approach best supports their long-term Enterprise Content Management strategy.</p>
<h3 id="heading-what-well-cover">What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-understanding-headless-cms-architecture">Understanding Headless CMS Architecture</a></p>
</li>
<li><p><a href="#heading-what-is-a-pure-headless-cms">What Is a Pure Headless CMS?</a></p>
</li>
<li><p><a href="#heading-what-is-a-hybrid-headless-cms">What Is a Hybrid Headless CMS?</a></p>
</li>
<li><p><a href="#heading-comparing-content-creation-and-editorial-experience">Comparing Content Creation and Editorial Experience</a></p>
</li>
<li><p><a href="#heading-developer-flexibility-and-customisation">Developer Flexibility and Customisation</a></p>
</li>
<li><p><a href="#heading-enterprise-content-governance-and-compliance">Enterprise Content Governance and Compliance</a></p>
</li>
<li><p><a href="#heading-content-localisation-and-global-operations">Content Localisation and Global Operations</a></p>
</li>
<li><p><a href="#heading-supporting-composable-architecture-initiatives">Supporting Composable Architecture Initiatives</a></p>
</li>
<li><p><a href="#heading-which-architecture-is-right-for-your-enterprise">Which Architecture Is Right for Your Enterprise?</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-understanding-headless-cms-architecture"><strong>Understanding Headless CMS Architecture</strong></h2>
<p>A <a href="https://www.ibm.com/think/topics/content-management-system">traditional CMS</a> like WordPress and Ghost combines content management and content presentation within a single system. The backend stores content, while the frontend controls how that content is displayed.</p>
<p>A <a href="https://www.wix.com/studio/blog/headless-cms">headless CMS</a> like Strapi and Contentful removes the presentation layer entirely. Content is managed in the backend and delivered through APIs to any frontend application.</p>
<p>This API-First CMS approach gives developers greater flexibility. Instead of relying on templates built into the CMS, teams can create custom experiences using modern frameworks such as React, Angular, or Vue, or by building native mobile applications.</p>
<p>Headless systems align closely with modern Composable Architecture strategies, where organisations assemble best-of-breed technologies rather than relying on a single monolithic platform.</p>
<p>Despite sharing this common foundation, Pure Headless and Hybrid Headless architectures take different approaches to balancing flexibility and content management capabilities.</p>
<h2 id="heading-what-is-a-pure-headless-cms"><strong>What Is a Pure Headless CMS?</strong></h2>
<p>A Pure Headless CMS focuses entirely on content storage, organisation, and delivery through APIs. As organisations modernise their <a href="https://www.coremedia.com/blog/the-7-best-cms-platforms-for-enterprises">enterprise content management platforms</a>, many are adopting pure headless architectures to gain greater flexibility in how content is delivered across digital channels.</p>
<p>In this model, the CMS doesn't provide website rendering, page management, visual editing, or presentation tools. Content creators manage structured content, while developers build separate frontend applications that consume it via APIs.</p>
<p>The primary goal is to create a clean separation between content and presentation.</p>
<p>A Pure Headless CMS is particularly attractive for organisations with strong development teams and highly customised digital experiences. Since there are no restrictions imposed by a built-in presentation layer, developers have complete freedom to design user experiences across multiple channels.</p>
<p>This approach supports true <a href="https://wpvip.com/blog/omnichannel-content-management/">Omnichannel Content Delivery</a> because the same content repository can power websites, mobile apps, digital signage, voice assistants, and future channels that may not yet exist.</p>
<p>But this flexibility often comes with tradeoffs. Marketing teams may become dependent on developers for tasks that would otherwise be handled through visual editing tools. Content preview capabilities can also be limited compared to more integrated solutions.</p>
<h2 id="heading-what-is-a-hybrid-headless-cms"><strong>What Is a Hybrid Headless CMS?</strong></h2>
<p>A Hybrid Headless CMS combines the API-driven capabilities of headless architecture with traditional CMS features.</p>
<p>Like a pure headless platform, content can be delivered through APIs to multiple channels. But hybrid systems also provide optional presentation capabilities, visual editing interfaces, page management tools, and content previews.</p>
<p>This dual approach allows organisations to support both developer-driven applications and marketer-friendly content management workflows.</p>
<p>A Hybrid Headless CMS, like Coremedia or Optimizely, enables teams to choose the most appropriate content delivery method for each use case. Some experiences can be delivered through APIs, while others can leverage built-in rendering capabilities.</p>
<p>For many enterprises, this balance reduces operational complexity while maintaining the flexibility needed for modern digital experiences.</p>
<p>Hybrid platforms are increasingly becoming a core component of broader Digital Experience Platform (DXP) strategies because they address both technical and business requirements.</p>
<h2 id="heading-comparing-content-creation-and-editorial-experience"><strong>Comparing Content Creation and Editorial Experience</strong></h2>
<p>One of the most significant differences in any Headless CMS Comparison involves the content authoring experience.</p>
<p>In a Pure Headless CMS environment, content creators typically work with structured content models. They create and manage content independently of how it appears on end-user devices.</p>
<p>While this approach encourages content reuse and consistency, it can make it difficult for editors to visualise the final experience. Preview functionality often requires additional development work.</p>
<p>Hybrid Headless CMS platforms usually offer richer editorial tools. Editors can preview content before publication, manage page layouts, and collaborate more effectively with marketing teams.</p>
<p>For enterprises with large editorial organisations, these capabilities can significantly improve Content Workflow Management and reduce friction between technical and non-technical stakeholders.</p>
<p>Organisations should carefully evaluate whether developer flexibility or editorial efficiency represents the higher priority.</p>
<h2 id="heading-developer-flexibility-and-customisation"><strong>Developer Flexibility and Customisation</strong></h2>
<p>When evaluating CMS Architecture options, developer flexibility remains a major consideration.</p>
<p>Pure Headless CMS platforms offer maximum freedom. Development teams can select any frontend technology, framework, or architecture without limitations imposed by the CMS.</p>
<p>This flexibility is particularly valuable for organisations building complex digital ecosystems with unique user experiences.</p>
<p>Developers can independently optimise performance, security, scalability, and user interfaces while leveraging APIs for content retrieval.</p>
<p>Hybrid platforms also support modern frontend frameworks and API-based delivery. But some organisations may perceive certain built-in capabilities as adding additional complexity or reducing architectural purity.</p>
<p>In practice, many Hybrid Headless CMS solutions still provide substantial developer flexibility while offering tools that simplify content management operations.</p>
<p>The best choice often depends on how much control developers require and how much autonomy content teams need.</p>
<h2 id="heading-enterprise-content-governance-and-compliance"><strong>Enterprise Content Governance and Compliance</strong></h2>
<p>Governance becomes increasingly important as organisations scale content production across regions, departments, and channels.</p>
<p>Enterprise CMS platforms must support approval workflows, permissions, auditing, version control, and regulatory compliance requirements.</p>
<p>Pure Headless CMS platforms can support governance, but many organisations must integrate additional tools to achieve comprehensive oversight.</p>
<p>Hybrid Headless CMS solutions often include advanced CMS Governance features directly within the platform.</p>
<p>These capabilities help organisations maintain consistency, enforce content standards, and manage risk across large content ecosystems.</p>
<p>For regulated industries such as finance, healthcare, and pharmaceuticals, governance capabilities can become a deciding factor when selecting an Enterprise CMS.</p>
<p>Organisations that prioritise compliance and oversight should carefully assess governance requirements during vendor evaluations.</p>
<h2 id="heading-content-localisation-and-global-operations"><strong>Content Localisation and Global Operations</strong></h2>
<p>Global enterprises frequently manage content in dozens of languages and markets.</p>
<p>Content Localization is no longer limited to translation. It also involves regional customisation, legal compliance, cultural adaptation, and coordinated publishing schedules.</p>
<p>Pure Headless CMS platforms can support localisation through structured content models and API-based delivery. But localisation workflows may require additional integrations and custom development.</p>
<p>Hybrid systems often provide more comprehensive localisation management features, including translation workflows, language synchronisation, content previews, and market-specific publishing controls.</p>
<p>These capabilities streamline global content operations and reduce administrative overhead for multinational organisations.</p>
<p>As enterprises expand internationally, localisation support becomes a critical component of long-term content strategy.</p>
<h2 id="heading-supporting-composable-architecture-initiatives"><strong>Supporting Composable Architecture Initiatives</strong></h2>
<p>Many organisations are embracing Composable Architecture to improve agility and avoid vendor lock-in.</p>
<p>A composable approach allows businesses to assemble specialised tools for content management, personalisation, analytics, commerce, and customer engagement.</p>
<p>Pure Headless CMS platforms naturally align with composable strategies because they focus exclusively on content management and API delivery.</p>
<p>Hybrid platforms can also support composable environments while providing additional integrated capabilities.</p>
<p>The decision often depends on organisational maturity. Enterprises with sophisticated engineering teams may prefer assembling specialised components themselves. Organisations seeking faster implementation may benefit from the integrated capabilities offered by hybrid solutions.</p>
<p>Both approaches can successfully support modern composable ecosystems when implemented correctly.</p>
<h2 id="heading-which-architecture-is-right-for-your-enterprise"><strong>Which Architecture Is Right for Your Enterprise?</strong></h2>
<p>There is no universal answer to the Pure Headless versus Hybrid Headless debate.</p>
<p>A Pure Headless CMS may be the right choice when an organisation prioritises developer flexibility, custom frontend experiences, and extensive omnichannel delivery requirements. It works particularly well for companies with mature engineering resources and highly specialised digital products.</p>
<p>A Hybrid Headless CMS may be the better option when marketing teams require visual editing, content previews, workflow automation, and governance capabilities. It can reduce operational complexity while still supporting modern API-driven delivery models.</p>
<p>Many enterprises ultimately discover that business requirements extend beyond technical architecture alone. Editorial productivity, governance, localisation, compliance, and long-term scalability often play equally important roles in platform selection.</p>
<p>Organisations evaluating enterprise content management platforms should consider not only current requirements but also future growth, emerging channels, and evolving customer expectations.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>The evolution of digital experiences has transformed how enterprises approach content management. Traditional monolithic systems are giving way to more flexible architectures that support modern customer journeys across multiple channels.</p>
<p>Both Pure Headless CMS and Hybrid Headless CMS solutions offer significant advantages over legacy platforms, but they serve different organisational needs.</p>
<p>Pure headless architectures emphasise flexibility, customisation, and API-first development. Hybrid architectures balance those capabilities with stronger editorial experiences, governance controls, and content management functionality.</p>
<p>The most successful Enterprise Content Management strategies align technology choices with business objectives. By carefully evaluating developer needs, content operations, governance requirements, localisation demands, and composable architecture goals, organisations can select a CMS architecture that supports sustainable growth and exceptional digital experiences for years to come.</p>
<p>For organisations researching enterprise content management platforms, understanding these architectural differences is an essential first step toward building a scalable and future-ready digital ecosystem.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How Large-Scale Platforms Handle Millions of Daily Transactions ]]>
                </title>
                <description>
                    <![CDATA[ Every day, millions of people order food, stream videos, send messages, book rides, make payments, and shop online. Most of these actions take only a few seconds from the user's perspective. A user cl ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-large-scale-platforms-handle-millions-of-daily-transactions/</link>
                <guid isPermaLink="false">6a2cfda7306003b984294a7b</guid>
                
                    <category>
                        <![CDATA[ software architecture ]]>
                    </category>
                
                    <category>
                        <![CDATA[ scaling ]]>
                    </category>
                
                    <category>
                        <![CDATA[ infrastructure ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Reliability ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Sat, 13 Jun 2026 06:50:15 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/67e3b365-0795-4055-9a59-61e32090de3e.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Every day, millions of people order food, stream videos, send messages, book rides, make payments, and shop online. Most of these actions take only a few seconds from the user's perspective. A user clicks a button, and the platform responds almost instantly.</p>
<p>Behind the scenes, however, these platforms are processing enormous numbers of transactions. A single popular application may handle thousands of requests every second and millions of transactions every day. Each transaction must be processed accurately, securely, and quickly.</p>
<p>In this article, we'll explore how large-scale platforms manage massive transaction volumes, the engineering challenges involved, and the architectural patterns developers use to build reliable systems.</p>
<h3 id="heading-what-well-cover">What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-why-transaction-volume-creates-unique-challenges">Why Transaction Volume Creates Unique Challenges</a></p>
</li>
<li><p><a href="#heading-breaking-monoliths-into-services">Breaking Monoliths Into Services</a></p>
</li>
<li><p><a href="#heading-using-load-balancers-to-distribute-traffic">Using Load Balancers to Distribute Traffic</a></p>
</li>
<li><p><a href="#heading-why-databases-become-bottlenecks">Why Databases Become Bottlenecks</a></p>
</li>
<li><p><a href="#heading-caching-frequently-accessed-data">Caching Frequently Accessed Data</a></p>
</li>
<li><p><a href="#heading-processing-tasks-asynchronously">Processing Tasks Asynchronously</a></p>
</li>
<li><p><a href="#heading-preventing-duplicate-transactions">Preventing Duplicate Transactions</a></p>
</li>
<li><p><a href="#heading-monitoring-everything">Monitoring Everything</a></p>
</li>
<li><p><a href="#heading-preparing-for-traffic-spikes">Preparing for Traffic Spikes</a></p>
</li>
<li><p><a href="#heading-building-for-failure">Building for Failure</a></p>
</li>
<li><p><a href="#heading-the-importance-of-consistency-and-reliability">The Importance of Consistency and Reliability</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-why-transaction-volume-creates-unique-challenges">Why Transaction Volume Creates Unique Challenges</h2>
<p>Handling a few hundred transactions per day is relatively straightforward. A single server and database can often manage the workload without difficulty. The challenge emerges as usage grows and systems begin serving thousands or even millions of users simultaneously.</p>
<p>Consider an online marketplace operating across multiple countries. At any given moment, thousands of users may be placing orders. Inventory must be updated in real time, payments must be processed accurately, notifications must be delivered, and fraud detection systems must evaluate transactions before approval. All of this happens within seconds.</p>
<p>At scale, even a minor delay can affect thousands of users. Systems must maintain low response times while preventing database bottlenecks, avoiding duplicate transactions, handling unexpected traffic spikes, and remaining reliable when failures occur.</p>
<p>To solve these problems, engineering teams rely on <a href="https://www.atlassian.com/microservices/microservices-architecture/distributed-architecture">distributed systems</a> and scalable architectural patterns.</p>
<h2 id="heading-breaking-monoliths-into-services">Breaking Monoliths Into Services</h2>
<p>Many successful platforms begin as <a href="https://www.freecodecamp.org/news/microservices-vs-monoliths-explained/#heading-what-is-a-monolith">monolithic applications</a> where all functionality exists within a single codebase. While this approach works well during the early stages of growth, it can become increasingly difficult to scale as transaction volume increases.</p>
<p>To overcome this limitation, large platforms often adopt a service-oriented architecture. Instead of one application handling every responsibility, individual services are created for specific business functions such as user management, payments, inventory, notifications, and analytics.</p>
<p>A simplified order-processing workflow might look like this:</p>
<pre><code class="language-python">def create_order(user_id, product_id):
    inventory.reserve(product_id)

    payment_result = payment.charge(user_id)

    if payment_result.success:
        order.create(user_id, product_id)
        notification.send_confirmation(user_id)

    return payment_result
</code></pre>
<p>This separation allows each service to scale independently. If payment activity suddenly increases, engineers can allocate additional resources specifically to the payment service without affecting the rest of the platform. It also lets teams develop, deploy, and maintain services independently, improving both agility and reliability.</p>
<h2 id="heading-using-load-balancers-to-distribute-traffic">Using Load Balancers to Distribute Traffic</h2>
<p>No single server can handle millions of daily transactions on its own. To distribute incoming requests efficiently, platforms place <a href="https://www.freecodecamp.org/news/auto-scaling-and-load-balancing/#heading-load-balancing-explained">load balancers</a> in front of their application servers.</p>
<p>Instead of connecting directly to a server, users send requests to a load balancer. The load balancer determines which server is best positioned to handle each request based on factors such as current load, availability, and health status.</p>
<p>A simplified architecture looks like this:</p>
<pre><code class="language-text">Users
   |
Load Balancer
   |
-------------------
|        |        |
Server1 Server2 Server3
</code></pre>
<p>If one server becomes overloaded or fails, traffic can be redirected to healthier servers. This improves both performance and availability. Modern cloud providers offer managed load-balancing solutions that automatically distribute traffic based on resource utilization and server health.</p>
<h2 id="heading-why-databases-become-bottlenecks">Why Databases Become Bottlenecks</h2>
<p>Scaling application servers is often relatively easy. But databases frequently become the most significant bottleneck in transaction-heavy systems.</p>
<p>Every transaction ultimately requires reading or writing data. Consider an <a href="https://jumptask.io/blog/guide-to-task-earning/">online task management platform</a> where users complete tasks and receive rewards. Each completed task may trigger multiple database operations, including verification of task completion, updating account balances, recording transaction history, and generating audit logs.</p>
<p>As transaction volume grows, database performance becomes critical. One common solution is read replication. Instead of relying on a single database instance, platforms create multiple replicas that handle read requests while the primary database focuses on write operations.</p>
<p>The architecture may resemble the following:</p>
<pre><code class="language-text">Primary DB
     |
-------------------------
|         |            |
Replica1 Replica2 Replica3
</code></pre>
<p>By distributing read traffic across multiple replicas, platforms reduce pressure on the primary database and improve response times for users.</p>
<h2 id="heading-caching-frequently-accessed-data">Caching Frequently Accessed Data</h2>
<p>Not every request needs to reach the database. In fact, repeatedly querying the database for the same information can significantly increase infrastructure costs and response times.</p>
<p>To address this, platforms use <a href="https://www.freecodecamp.org/news/how-in-memory-caching-works-in-redis/">caching systems such as Redis</a> to store frequently accessed data in memory. Information such as user profiles, product details, and application settings often changes infrequently and can be retrieved directly from the cache.</p>
<p>Without caching:</p>
<pre><code class="language-python">user = database.get_user(user_id)
</code></pre>
<p>With caching:</p>
<pre><code class="language-python">user = cache.get(user_id)

if not user:
    user = database.get_user(user_id)
    cache.set(user_id, user)
</code></pre>
<p>Memory access is substantially faster than database queries. When a platform processes millions of requests every day, caching can dramatically improve performance while reducing backend load.</p>
<h2 id="heading-processing-tasks-asynchronously">Processing Tasks Asynchronously</h2>
<p>Users expect immediate responses. If every operation must finish before the system responds, applications quickly become sluggish under heavy load.</p>
<p>To improve responsiveness, large-scale systems separate critical user-facing actions from background processing tasks. Consider a payment transaction. The user needs confirmation that the payment was successful, but they don't need to wait for analytics updates, report generation, or email delivery.</p>
<p>A synchronous implementation might look like this:</p>
<pre><code class="language-python">process_payment()
send_email()
update_analytics()
generate_report()
</code></pre>
<p>A more scalable approach uses <a href="https://www.freecodecamp.org/news/how-message-queues-make-distributed-systems-more-reliable/">message queues</a>:</p>
<pre><code class="language-python">process_payment()

queue.publish("send_email")
queue.publish("update_analytics")
queue.publish("generate_report")
</code></pre>
<p>Background workers consume these queued tasks and process them independently. This architecture improves user experience and enables systems to handle significantly larger transaction volumes.</p>
<h2 id="heading-preventing-duplicate-transactions">Preventing Duplicate Transactions</h2>
<p>One of the most important challenges in transaction processing is preventing duplicate execution.</p>
<p>Network interruptions can create situations where users unknowingly submit the same request multiple times. Imagine a customer making a purchase. The payment succeeds, but the confirmation never reaches the user's device because of a network failure. Believing the payment failed, the customer clicks the button again.</p>
<p>Without safeguards, the platform could charge the customer twice.</p>
<p>Many systems solve this problem through <a href="https://temporal.io/blog/idempotency-and-durable-execution">idempotency</a> keys. A simplified implementation looks like this:</p>
<pre><code class="language-python">def process_payment(request_id, amount):

    if payment_exists(request_id):
        return existing_payment(request_id)

    payment = create_payment(request_id, amount)
    return payment
</code></pre>
<p>If the same request arrives again, the system returns the original result instead of processing a second payment. This pattern is widely used in financial services, payment gateways, and banking applications.</p>
<h2 id="heading-monitoring-everything">Monitoring Everything</h2>
<p>As systems grow more complex, visibility becomes essential. Engineering teams can't effectively troubleshoot issues they can't observe.</p>
<p>Modern platforms collect metrics from every layer of their infrastructure. Engineers <a href="https://www.freecodecamp.org/news/the-front-end-monitoring-handbook/">continuously monitor</a> request latency, database response times, error rates, queue depth, CPU utilization, and memory consumption.</p>
<p>A simple monitoring rule might look like this:</p>
<pre><code class="language-python">if error_rate &gt; 5:
    alert("High error rate detected")
</code></pre>
<p>Monitoring enables teams to identify problems before they impact users. It also provides valuable data for performance optimization and future capacity planning.</p>
<h2 id="heading-preparing-for-traffic-spikes">Preparing for Traffic Spikes</h2>
<p>Traffic patterns are rarely predictable. An e-commerce platform may experience enormous demand during holiday sales, while a ticketing website can receive millions of requests within minutes when a popular event goes live.</p>
<p>To handle these surges, platforms rely on autoscaling. Cloud infrastructure can automatically add resources as demand increases and remove them when traffic subsides.</p>
<p>A simplified scaling rule might look like this:</p>
<pre><code class="language-python">if cpu_usage &gt; 70:
    add_server()
</code></pre>
<p>Autoscaling helps maintain performance during peak periods while controlling infrastructure costs during quieter times.</p>
<h2 id="heading-building-for-failure">Building for Failure</h2>
<p>One of the most important principles in distributed systems is accepting that failures are inevitable.</p>
<p>Servers crash. Databases become unavailable. Networks experience interruptions. Rather than hoping these events never occur, large-scale platforms design systems that can continue operating when failures happen.</p>
<p>For example, payment systems often include retry logic:</p>
<pre><code class="language-python">for attempt in range(3):
    try:
        charge_customer()
        break
    except:
        continue
</code></pre>
<p>In addition, platforms implement redundancy by running multiple instances of critical components across different geographic regions and availability zones. If one component fails, another can take over with minimal disruption.</p>
<p>This strategy significantly improves availability and resilience.</p>
<h2 id="heading-the-importance-of-consistency-and-reliability">The Importance of Consistency and Reliability</h2>
<p>At scale, transaction processing isn't solely about speed. Accuracy is equally important.</p>
<p>Users may tolerate a slight delay, but they won't tolerate duplicate charges, missing funds, incorrect balances, or lost transactions. For this reason, large-scale transaction systems place a strong emphasis on consistency, auditing, logging, reconciliation, and recovery mechanisms.</p>
<p>Every transaction must be traceable. Every failure must be recoverable. These requirements become particularly important in industries such as finance, e-commerce, subscription billing, and task earning platforms where money and rewards move between users and businesses every day.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>The ability to handle millions of daily transactions isn't the result of a single technology. It comes from combining multiple architectural principles that work together to create reliable, scalable systems.</p>
<p>Large-scale platforms distribute traffic across multiple servers, separate responsibilities into specialized services, cache frequently accessed data, process background work asynchronously, continuously monitor system health, and design for inevitable failures.</p>
<p>For developers, understanding these patterns provides valuable insight into how modern internet platforms operate behind the scenes. Whether you're building a payment processor, a SaaS platform, an online marketplace, or a task earning application, the same foundational principles apply.</p>
<p>As systems grow, scalability becomes less about writing more code and more about designing architecture that remains reliable under increasing demand. The platforms that succeed are the ones capable of delivering fast, accurate, and consistent transactions regardless of how many users arrive.</p>
<p>Hope you enjoyed this article. You can <a href="https://linkedin.com/in/manishmshiva">connect with me on LinkedIn</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Set Up OpenClaw and Design an A2A Plugin Bridge ]]>
                </title>
                <description>
                    <![CDATA[ OpenClaw is getting attention because it turns a popular AI idea into something you can actually run yourself. Instead of opening one more browser tab, you run a Gateway on your own machine or server  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/openclaw-a2a-plugin-architecture-guide/</link>
                <guid isPermaLink="false">69d542ca5da14bc70e7c1559</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Open Source ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software architecture ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nataraj Sundar ]]>
                </dc:creator>
                <pubDate>Tue, 07 Apr 2026 17:45:46 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/4be03b02-d128-49e9-afcb-fea0f771e746.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>OpenClaw is getting attention because it turns a popular AI idea into something you can actually run yourself. Instead of opening one more browser tab, you run a Gateway on your own machine or server and connect it to communication tools you already use.</p>
<p>That matters because OpenClaw is self-hosted, multi-channel, open source, and built around agent workflows such as sessions, tools, plugins, and multi-agent routing. It feels less like a toy chatbot and more like an operator-controlled agent runtime.</p>
<p>In this guide, you'll do three things. First, you'll learn what OpenClaw is and why developers are paying attention to it. Second, you'll get it running the beginner-friendly way through the dashboard. Third, you'll walk through an original design contribution: a proposed OpenClaw-to-A2A plugin architecture and a <a href="https://github.com/natarajsundar/openclaw-a2a-secure-agent-runtime"><code>proof-of-concept</code></a> relay that shows how OpenClaw’s session model could map to the A2A protocol.</p>
<p>That last part is important, so I want to frame it carefully. The A2A integration in this article is <strong>not</strong> presented as a built-in OpenClaw feature. It's a documented architecture proposal built on top of the extension points OpenClaw already exposes.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>This guide is beginner-friendly for OpenClaw itself, but it assumes a few basics so you can follow the architecture and proof-of-concept sections comfortably.</p>
<p>Before you continue, you should be familiar with:</p>
<ul>
<li><p>Basic JavaScript or Node.js (reading and running scripts)</p>
</li>
<li><p>How HTTP APIs work (requests, responses, JSON payloads)</p>
</li>
<li><p>Using a terminal to run commands</p>
</li>
<li><p>High-level concepts like services, APIs, or microservices</p>
</li>
</ul>
<p>You don't need prior experience with OpenClaw or A2A. The setup steps walk through everything you need to get started.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-what-openclaw-is">What OpenClaw Is</a></p>
</li>
<li><p><a href="#heading-why-openclaw-is-getting-so-much-attention">Why OpenClaw Is Getting So Much Attention</a></p>
</li>
<li><p><a href="#heading-what-the-a2a-protocol-is">What the A2A Protocol Is</a></p>
</li>
<li><p><a href="#heading-how-openclaw-and-a2a-relate">How OpenClaw and A2A Relate</a></p>
</li>
<li><p><a href="#heading-what-you-need-before-you-start">What You Need Before You Start</a></p>
</li>
<li><p><a href="#heading-step-1-install-openclaw">Install OpenClaw</a></p>
</li>
<li><p><a href="#heading-step-2-run-the-onboarding-wizard">Run the Onboarding Wizard</a></p>
</li>
<li><p><a href="#heading-step-3-check-the-gateway-and-open-the-dashboard">Check the Gateway and Open the Dashboard</a></p>
</li>
<li><p><a href="#heading-step-4-use-openclaw-as-a-private-coding-assistant">Use OpenClaw as a Private Coding Assistant</a></p>
</li>
<li><p><a href="#heading-step-5-understand-multi-agent-routing">Understand Multi Agent Routing</a></p>
</li>
<li><p><a href="#heading-where-a2a-could-fit-later">Where A2A Could Fit Later</a></p>
</li>
<li><p><a href="#heading-a-proposed-openclaw-to-a2a-plugin-architecture">A Proposed OpenClaw to A2A Plugin Architecture</a></p>
</li>
<li><p><a href="#heading-build-the-proof-of-concept-relay">Build the Proof of Concept Relay</a></p>
</li>
<li><p><a href="#heading-how-the-proof-of-concept-maps-to-a-real-openclaw-plugin">How the Proof of Concept Maps to a Real OpenClaw Plugin</a></p>
</li>
<li><p><a href="#heading-security-notes-before-you-go-further">Security Notes Before You Go Further</a></p>
</li>
<li><p><a href="#heading-final-thoughts">Final Thoughts</a></p>
</li>
</ol>
<h2 id="heading-what-openclaw-is">What OpenClaw Is</h2>
<p>According to the <a href="https://docs.openclaw.ai/">official docs</a>, OpenClaw is a self-hosted gateway that connects chat apps like WhatsApp, Telegram, Discord, iMessage, and a browser dashboard to AI agents.</p>
<p>That wording is useful because it tells you where OpenClaw sits in the stack. It's not just a model wrapper. It's a Gateway that handles sessions, routing, and app connections, while agents, tools, plugins, and providers do the actual work.</p>
<p>Here is the simplest mental model:</p>
<img src="https://cdn.hashnode.com/uploads/covers/694ca88d5ac09a5d68c63854/ad5f3295-8fdf-4f9c-8488-f69808850295.png" alt="Diagram showing OpenClaw architecture where multiple chat apps and a browser dashboard connect to a central Gateway, which routes requests to different agents that use model providers and tools." style="display: block;" width="1097" height="462" loading="lazy">

<p>If you're new to the project, this is the practical way to think about it:</p>
<ul>
<li><p>your chat apps are the front door</p>
</li>
<li><p>the Gateway is the traffic and control layer</p>
</li>
<li><p>the agent is the reasoning layer</p>
</li>
<li><p>the model provider and tools are what let the agent actually do work</p>
</li>
</ul>
<p>That's one reason OpenClaw feels different from a normal browser-only assistant.</p>
<h2 id="heading-why-developers-are-paying-attention-to-openclaw">Why Developers Are Paying Attention to OpenClaw</h2>
<p>OpenClaw is getting a lot of attention for a few reasons.</p>
<p>The first reason is control. The docs position OpenClaw as self-hosted and multi-channel, which means you can run it on your own machine or server instead of depending on a fully hosted assistant.</p>
<p>The second reason is that OpenClaw already looks like an agent platform. The docs talk about sessions, plugins, tools, skills, multi-agent routing, and ACP-backed external coding harnesses. That's a much richer story than “ask a model a question in a web page.”</p>
<p>The third reason is workflow fit. A lot of people don't want another inbox. They want an assistant that can live in the tools they already check every day.</p>
<p>There's also a broader industry trend behind the hype. Developers are actively looking for ways to connect multiple agents and multiple tools without giving up visibility into what's happening. OpenClaw sits directly in that conversation.</p>
<h2 id="heading-what-the-a2a-protocol-is">What the A2A Protocol Is</h2>
<p>A2A, short for Agent2Agent, is an open protocol for communication between agent systems. The <a href="https://a2a-protocol.org/latest/specification/">A2A specification</a> says its purpose is to help independent agent systems discover each other, negotiate interaction modes, manage collaborative tasks, and exchange information without exposing internal memory, tools, or proprietary logic.</p>
<p>That last point matters. A2A is about interoperability between agent systems, not about exposing all of one agent's internals to another.</p>
<p>A2A introduces a few core concepts that are worth learning early:</p>
<ul>
<li><p><strong>Agent Card</strong>: a JSON description of the remote agent, its URL, skills, capabilities, and auth requirements</p>
</li>
<li><p><strong>Task</strong>: the main unit of remote work</p>
</li>
<li><p><strong>Artifact</strong>: the output of a task</p>
</li>
<li><p><strong>Context ID</strong>: a stable interaction boundary across multiple related turns</p>
</li>
</ul>
<p>A2A tasks follow a fairly clean lifecycle:</p>
<img src="https://cdn.hashnode.com/uploads/covers/694ca88d5ac09a5d68c63854/3b5a43e8-dabd-45e3-bff1-0081e2b37e0d.png" alt="State diagram illustrating the A2A task lifecycle including submitted, working, input required, completed, failed, rejected, and canceled states.." style="display: block;" width="598" height="380" loading="lazy">

<p>The A2A docs also explain that A2A and MCP are complementary, not competing. A2A is for agent-to-agent collaboration. MCP is for agent-to-tool communication.</p>
<p>That distinction is useful when you compare A2A with OpenClaw, because OpenClaw already has strong local tool and session concepts.</p>
<h2 id="heading-how-openclaw-and-a2a-relate">How OpenClaw and A2A Relate</h2>
<p>OpenClaw and A2A are not the same thing, but they line up in interesting ways.</p>
<p>OpenClaw already documents several features that point in a multi-agent direction:</p>
<ul>
<li><p><a href="https://docs.openclaw.ai/concepts/multi-agent/">multi-agent routing</a> for multiple isolated agents in one running Gateway</p>
</li>
<li><p><a href="https://docs.openclaw.ai/concepts/session-tool/">session tools</a> such as <code>sessions_send</code> and <code>sessions_spawn</code></p>
</li>
<li><p>a <a href="https://docs.openclaw.ai/tools/plugin/">plugin system</a> that can register tools, HTTP routes, Gateway RPC methods, and background services</p>
</li>
<li><p><a href="https://docs.openclaw.ai/tools/acp-agents/">ACP support</a> and the <a href="https://docs.openclaw.ai/cli/acp"><code>openclaw acp</code> bridge</a> for external coding clients</p>
</li>
</ul>
<p>But it's still important to stay precise here.</p>
<p>OpenClaw documents ACP, plugins, and local multi-agent coordination today. The docs I checked do <strong>not</strong> describe native A2A support as a first-class built-in capability.</p>
<p>That means the honest claim is this:</p>
<p><strong>OpenClaw can be meaningfully connected to A2A in theory because the architectural pieces line up, but the A2A bridge still has to be built.</strong></p>
<h3 id="heading-acp-versus-a2a">ACP versus A2A</h3>
<p>ACP and A2A solve different problems.</p>
<p>ACP in OpenClaw today is about bridging an IDE or coding client to a Gateway-backed session.</p>
<p>A2A is about one agent system talking to another agent system across a protocol boundary.</p>
<img src="https://cdn.hashnode.com/uploads/covers/694ca88d5ac09a5d68c63854/9790f239-528c-422f-bbc5-3e82c7f1a171.png" alt="Diagram showing A2A interaction where an OpenClaw agent communicates through a plugin to discover a remote agent via an Agent Card and send tasks for execution." style="display: block;" width="1232" height="233" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/694ca88d5ac09a5d68c63854/c4d4279b-3099-4c1b-92b6-3eaf817a6e84.png" alt="Diagram showing ACP flow where an IDE or coding client connects through an OpenClaw ACP bridge to a Gateway-backed session." style="display: block;" width="1179" height="215" loading="lazy">

<p>That difference is one reason I prefer the phrase <strong>plugin bridge</strong> here instead of <strong>native A2A support</strong>.</p>
<h2 id="heading-what-you-need-before-you-start">What You Need Before You Start</h2>
<p>The easiest first run does <strong>not</strong> require WhatsApp, Telegram, or Discord.</p>
<p>The OpenClaw onboarding docs say the fastest first chat is the dashboard. That makes this a much more approachable beginner setup.</p>
<p>Before you start, you'll need:</p>
<ol>
<li><p>Node 24 if possible, or Node 22.16+ for compatibility</p>
</li>
<li><p>an API key for the model provider you want to use</p>
</li>
<li><p>If you're on Windows, WSL2 is the recommended path for the full experience. Native Windows works for core CLI and Gateway flows, but the docs call out caveats and position WSL2 as the more stable setup.</p>
</li>
<li><p>about five minutes for the first dashboard-based run</p>
</li>
</ol>
<h2 id="heading-step-1-install-openclaw">Step 1: Install OpenClaw</h2>
<p>The official getting-started page recommends the installer script.</p>
<p>On macOS, Linux, or WSL2, run:</p>
<pre><code class="language-bash">curl -fsSL https://openclaw.ai/install.sh | bash
</code></pre>
<p>On Windows PowerShell, the docs show this:</p>
<pre><code class="language-powershell">iwr -useb https://openclaw.ai/install.ps1 | iex
</code></pre>
<p>If you're on Windows, the platform docs recommend installing WSL2 first:</p>
<pre><code class="language-powershell">wsl --install
</code></pre>
<p>Then open Ubuntu and continue with the Linux commands there.</p>
<h2 id="heading-step-2-run-the-onboarding-wizard">Step 2: Run the Onboarding Wizard</h2>
<p>Once the CLI is installed, run the onboarding wizard.</p>
<pre><code class="language-bash">openclaw onboard --install-daemon
</code></pre>
<p>The onboarding wizard is the recommended path in the docs. It configures auth, gateway settings, optional channels, skills, and workspace defaults in one guided flow.</p>
<p>The most beginner-friendly choice is to keep the first run simple. Don't worry about chat apps yet. Get the local Gateway working first.</p>
<h2 id="heading-step-3-check-the-gateway-and-open-the-dashboard">Step 3: Check the Gateway and Open the Dashboard</h2>
<p>After onboarding, verify that the Gateway is running.</p>
<pre><code class="language-bash">openclaw gateway status
</code></pre>
<p>Then open the dashboard:</p>
<pre><code class="language-bash">openclaw dashboard
</code></pre>
<p>The docs call this the fastest first chat because it avoids channel setup. It's also the safest way to start, because the dashboard is local and the OpenClaw docs clearly say the Control UI is an admin surface and should not be exposed publicly.</p>
<p>The beginner setup flow looks like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/694ca88d5ac09a5d68c63854/eab78250-65d6-4d97-be3d-bf7167b9099e.png" alt="Sequence diagram showing OpenClaw setup flow from installation and onboarding to starting the Gateway and opening the dashboard for the first chat." style="display: block;" width="1200" height="635" loading="lazy">

<p>If you can chat in the dashboard, your day-zero setup is working.</p>
<h2 id="heading-step-4-use-openclaw-as-a-private-coding-assistant">Step 4: Use OpenClaw as a Private Coding Assistant</h2>
<p>The best first use case is not to drop OpenClaw into a public group chat.</p>
<p>Use it as a private coding assistant in the dashboard.</p>
<p>For example, try a prompt like this:</p>
<blockquote>
<p>I am building a small Node.js utility that reads Markdown files and generates a table of contents. Turn this idea into a project plan, a README outline, and the first five implementation tasks.</p>
</blockquote>
<p>That kind of prompt is ideal for a first run because it gives you something concrete back right away.</p>
<p>You can also use it to:</p>
<ol>
<li><p>turn rough notes into a plan,</p>
</li>
<li><p>summarize a bug report into action items,</p>
</li>
<li><p>draft a README,</p>
</li>
<li><p>propose a folder structure, or</p>
</li>
<li><p>write a safe first implementation checklist.</p>
</li>
</ol>
<p>That is already enough to make OpenClaw useful before you touch any advanced protocol work.</p>
<h2 id="heading-step-5-understand-multi-agent-routing">Step 5: Understand Multi Agent Routing</h2>
<p>Once the basic setup is working, it helps to understand OpenClaw’s local multi-agent model.</p>
<p>The docs describe multi-agent routing as a way to run multiple isolated agents in one Gateway, with separate workspaces, state directories, and sessions.</p>
<p>That means you can imagine setups like this:</p>
<ul>
<li><p>a personal assistant</p>
</li>
<li><p>a coding assistant</p>
</li>
<li><p>a research assistant</p>
</li>
<li><p>an alerts assistant</p>
</li>
</ul>
<p>OpenClaw already has a model for that:</p>
<img src="https://cdn.hashnode.com/uploads/covers/694ca88d5ac09a5d68c63854/c640a7c4-0421-4513-a2c2-658916504e3b.png" alt="Diagram illustrating OpenClaw multi-agent routing where incoming messages are matched to different agents such as main, coding, and alerts, each with separate sessions." style="display: block;" width="663" height="588" loading="lazy">

<p>You don't need to set this up on day one.</p>
<p>But it matters for the A2A discussion, because once you understand how OpenClaw routes work between local agents, it becomes much easier to think about routing work to <strong>remote</strong> agents through a protocol like A2A.</p>
<h2 id="heading-where-a2a-could-fit-later">Where A2A Could Fit Later</h2>
<p>A2A could fit into OpenClaw in two broad ways.</p>
<h3 id="heading-option-1-openclaw-as-an-a2a-client">Option 1: OpenClaw as an A2A Client</h3>
<p>In this model, OpenClaw stays your personal edge assistant.</p>
<p>It receives a request from the dashboard or a chat app, decides the task needs a specialist, discovers a remote A2A agent through an Agent Card, sends the task, waits for updates or artifacts, and translates the result back into a normal OpenClaw reply.</p>
<img src="https://cdn.hashnode.com/uploads/covers/694ca88d5ac09a5d68c63854/99a2e611-54ac-4c0f-8f8f-c1ce3246bb96.png" alt="Diagram showing OpenClaw acting as an A2A client, delegating tasks from a local session to a remote agent via an Agent Card and returning results to the user." style="display: block;" width="1548" height="945" loading="lazy">

<p>This is the cleaner story for a personal assistant. OpenClaw stays the front door, and A2A becomes a delegation path behind the scenes.</p>
<h3 id="heading-option-2-openclaw-as-an-a2a-server">Option 2: OpenClaw as an A2A Server</h3>
<p>In this model, OpenClaw exposes some of its own capabilities to other agents.</p>
<p>A plugin could theoretically publish an A2A Agent Card, advertise a narrow skill set, accept A2A tasks, and map those tasks into OpenClaw sessions or sub-agent runs.</p>
<p>That's technically plausible because the plugin system can register HTTP routes, tools, Gateway methods, and background services.</p>
<p>It's also the riskier direction for a personal assistant, which is why I think <strong>client-first</strong> is the right starting point.</p>
<h2 id="heading-a-proposed-openclaw-to-a2a-plugin-architecture">A Proposed OpenClaw to A2A Plugin Architecture</h2>
<p>This section is my original contribution in the article.</p>
<p>I think the cleanest first architecture is <strong>not</strong> a full bidirectional bridge. It's a narrow outbound delegation plugin that lets OpenClaw call a small allowlist of remote A2A agents.</p>
<p>The design goal is simple:</p>
<p><strong>Reuse OpenClaw for user-facing conversations and local tool access, but use A2A only when a remote specialist agent is the best place to do the work.</strong></p>
<p>Here is the architecture I would start with:</p>
<img src="https://cdn.hashnode.com/uploads/covers/694ca88d5ac09a5d68c63854/e88f06dd-f108-48b2-a9ee-b74eac6b733b.png" alt="Architecture diagram of an OpenClaw-to-A2A plugin showing components such as delegation tool, policy engine, Agent Card cache, session-to-task mapper, task poller, and remote A2A agent." style="display: block;" width="1548" height="945" loading="lazy">

<h3 id="heading-why-this-design-is-a-good-fit-for-openclaw">Why This Design is a Good Fit for OpenClaw</h3>
<p>This proposal is grounded in extension points OpenClaw already documents.</p>
<p>A plugin can register:</p>
<ul>
<li><p>an <strong>agent tool</strong> for delegation,</p>
</li>
<li><p>a <strong>Gateway method</strong> for health and diagnostics,</p>
</li>
<li><p>an <strong>HTTP route</strong> for future callbacks or webhook verification, and</p>
</li>
<li><p>a <strong>background service</strong> for cache warming, task subscriptions, or cleanup.</p>
</li>
</ul>
<p>That means the bridge doesn't have to modify OpenClaw core to be credible.</p>
<h3 id="heading-the-mapping-table">The Mapping Table</h3>
<p>The most important design decision is how to map OpenClaw’s session model to A2A’s task model.</p>
<p>Here is the mapping I recommend:</p>
<table>
<thead>
<tr>
<th>OpenClaw concept</th>
<th>A2A concept</th>
<th>Why this mapping works</th>
</tr>
</thead>
<tbody><tr>
<td><code>sessionKey</code></td>
<td><code>contextId</code></td>
<td>A single OpenClaw conversation should keep a stable remote context across related delegated turns</td>
</tr>
<tr>
<td>one delegated remote call</td>
<td>one <code>Task</code></td>
<td>each remote specialization request becomes a discrete unit of work</td>
</tr>
<tr>
<td>plugin tool call</td>
<td><code>SendMessage</code></td>
<td>the delegation tool is the natural point where the local agent crosses the protocol boundary</td>
</tr>
<tr>
<td>remote output</td>
<td><code>Artifact</code></td>
<td>A2A wants task outputs returned as artifacts rather than chat-only replies</td>
</tr>
<tr>
<td>plugin HTTP route</td>
<td>callback or future push handler</td>
<td>gives you a place to verify webhooks if you later adopt async push</td>
</tr>
<tr>
<td>Gateway method</td>
<td>status endpoint</td>
<td>gives operators a direct way to inspect relay health without going through the model</td>
</tr>
<tr>
<td>background service</td>
<td>polling or cache work</td>
<td>keeps asynchronous and maintenance work out of the tool call path</td>
</tr>
</tbody></table>
<p>This is the key architectural claim in the article:</p>
<p><strong>Treat the OpenClaw session as the long-lived conversational boundary, and treat each remote A2A task as one delegated execution inside that boundary.</strong></p>
<p>That preserves both sides cleanly.</p>
<h3 id="heading-the-design-in-one-sentence">The Design in One Sentence</h3>
<p>The <code>a2a_delegate</code> tool should:</p>
<ol>
<li><p>resolve an allowlisted remote Agent Card,</p>
</li>
<li><p>reuse an existing A2A <code>contextId</code> for the current <code>sessionKey</code> when possible,</p>
</li>
<li><p>create a fresh remote <code>Task</code> for the new delegated turn,</p>
</li>
<li><p>normalize remote artifacts back into a simple local answer, and</p>
</li>
<li><p>never expose the whole OpenClaw Gateway directly to the public internet.</p>
</li>
</ol>
<p>I like this design because it is incremental, testable, and consistent with OpenClaw’s personal-assistant trust model.</p>
<h2 id="heading-build-the-proof-of-concept-relay">Build the Proof of Concept Relay</h2>
<p>To make the architecture concrete, I built a small proof-of-concept relay.</p>
<p><a href="https://github.com/natarajsundar/openclaw-a2a-secure-agent-runtime">https://github.com/natarajsundar/openclaw-a2a-secure-agent-runtime</a></p>
<p>It's intentionally small. It doesn't try to become a full production plugin. Instead, it proves the hardest conceptual part of the bridge: how to map one OpenClaw session to a reusable A2A context while creating a fresh A2A task per delegated turn.</p>
<p>Here's the repository layout:</p>
<pre><code class="language-plaintext">openclaw-a2a-secure-agent-runtime/
├── README.md
├── package.json
├── examples/
│   └── openclaw-plugin-entry.example.ts
├── src/
│   ├── a2a-client.mjs
│   ├── agent-card-cache.mjs
│   ├── demo.mjs
│   ├── mock-remote-agent.mjs
│   ├── openclaw-a2a-relay.mjs
│   ├── session-task-map.mjs
│   └── utils.mjs
└── test/
    └── relay.test.mjs
</code></pre>
<p>The PoC does six things:</p>
<ol>
<li><p>fetches a remote Agent Card from <code>/.well-known/agent-card.json</code>,</p>
</li>
<li><p>caches it with simple <code>ETag</code> revalidation,</p>
</li>
<li><p>records local <code>sessionKey</code> to remote <code>contextId</code> mappings,</p>
</li>
<li><p>sends an A2A <code>SendMessage</code> request,</p>
</li>
<li><p>polls <code>GetTask</code> until the task finishes, and</p>
</li>
<li><p>converts the remote artifact into a local text answer.</p>
</li>
</ol>
<h3 id="heading-run-the-demo">Run the Demo</h3>
<p>The repo uses only built-in Node.js modules.</p>
<pre><code class="language-shell">cd openclaw-a2a-secure-agent-runtime
npm run demo
</code></pre>
<p>The demo spins up a mock remote A2A server, delegates one task, delegates a second task from the <strong>same</strong> local session, and shows that the same remote <code>contextId</code> is reused.</p>
<h3 id="heading-the-core-relay-idea">The Core Relay Idea</h3>
<p>This is the important logic in plain English:</p>
<ol>
<li><p>look up the most recent remote mapping for the current OpenClaw <code>sessionKey</code></p>
</li>
<li><p>reuse the old <code>contextId</code> if one exists</p>
</li>
<li><p>create a fresh A2A <code>Task</code> for the new request</p>
</li>
<li><p>poll until that task becomes <code>TASK_STATE_COMPLETED</code></p>
</li>
<li><p>turn the returned artifact into a normal text result that OpenClaw can send back to the user</p>
</li>
</ol>
<p>That makes the bridge predictable.</p>
<p>Here's a shortened version of the relay logic:</p>
<pre><code class="language-js">const previous = await sessionTaskMap.latestForSession(sessionKey, remoteBaseUrl);
const contextId = previous?.contextId ?? crypto.randomUUID();

const sendResult = await client.sendMessage({
  text,
  contextId,
  metadata: {
    openclawSessionKey: sessionKey,
    requestedSkillId: skillId,
  },
});

let task = sendResult.task;
while (!isTerminalTaskState(task.status?.state)) {
  await sleep(pollIntervalMs);
  task = await client.getTask(task.id);
}

return {
  contextId,
  taskId: task.id,
  answer: taskArtifactsToText(task),
};
</code></pre>
<p>That's the heart of the design.</p>
<h3 id="heading-why-this-repo-is-a-useful-proof-of-concept">Why This Repo is a Useful Proof of Concept</h3>
<p>A lot of “integration” articles stay too abstract. This repo avoids that problem in three ways.</p>
<p>First, it makes the session-to-context mapping explicit.</p>
<p>Second, it includes a mock remote A2A agent so you can test the flow without needing a large external setup.</p>
<p>Third, it includes a test that checks the most important invariant: repeated delegations from one local OpenClaw session reuse the same A2A context.</p>
<p>That is the piece I most wanted to make concrete, because it is where architecture turns into implementation.</p>
<h2 id="heading-how-the-proof-of-concept-maps-to-a-real-openclaw-plugin">How the Proof of Concept Maps to a Real OpenClaw Plugin</h2>
<p>The proof of concept is the relay core.</p>
<p>A real OpenClaw plugin would wrap that relay with four extension surfaces that the OpenClaw docs already describe.</p>
<h3 id="heading-1-a-delegation-tool">1: A Delegation Tool</h3>
<p>This is the main entry point.</p>
<p>A plugin would register an optional tool like <code>a2a_delegate</code> so the local agent can explicitly choose to delegate work.</p>
<p>That tool should be optional, not always-on, because remote delegation is a side effect and should be easy to disable.</p>
<h3 id="heading-2-a-gateway-method-for-diagnostics">2: A Gateway Method for Diagnostics</h3>
<p>A method like <code>a2a.status</code> would let you inspect whether the relay is healthy, which remote cards are cached, and whether any tasks are still being tracked.</p>
<p>That is much better than asking the model to “tell me if the bridge is healthy.”</p>
<h3 id="heading-3-a-plugin-http-route">3: A Plugin HTTP Route</h3>
<p>You may not need this on day one.</p>
<p>But once you move beyond polling and want push-style callbacks or webhook verification, a plugin route gives you the right boundary for that work.</p>
<h3 id="heading-4-a-background-service">4: A Background Service</h3>
<p>A small service is a clean place to do cache warming, cleanup, or later subscription handling.</p>
<p>That keeps the tool path focused on delegation instead of maintenance work.</p>
<p>If I were turning this into a real plugin package, I would sequence the work in this order:</p>
<ol>
<li><p>wrap the relay in <code>registerTool</code>,</p>
</li>
<li><p>add a small config schema with an allowlist of remote agents,</p>
</li>
<li><p>add <code>a2a.status</code>,</p>
</li>
<li><p>keep polling as the first async model,</p>
</li>
<li><p>add a callback route only if a real use case needs it.</p>
</li>
</ol>
<p>That is the most practical path from theory to a real extension.</p>
<p>I tested the relay flow locally with the mock remote agent and confirmed that repeated delegations from the same local session reused the same remote <code>contextId</code>.</p>
<h2 id="heading-security-notes-before-you-go-further">Security Notes Before You Go Further</h2>
<p>This is the section you should not skip.</p>
<p>The OpenClaw security docs explicitly say the project assumes a <strong>personal assistant</strong> trust model: one trusted operator boundary per Gateway. They also say a shared Gateway for mutually untrusted or adversarial users is not the supported boundary model.</p>
<p>That has a direct consequence for A2A.</p>
<p>A2A is designed for communication across agent systems and organizational boundaries. That is powerful, but it is also a different threat model from a single private OpenClaw deployment.</p>
<p>So the safer design is <strong>not</strong> this:</p>
<ul>
<li><p>expose your personal OpenClaw Gateway publicly,</p>
</li>
<li><p>let arbitrary remote agents reach it,</p>
</li>
<li><p>and hope the tool boundaries are enough.</p>
</li>
</ul>
<p>The safer design is closer to this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/694ca88d5ac09a5d68c63854/5ab4460a-6c00-4880-a29c-ddc1db00b5fa.png" alt="Diagram illustrating separation between a private OpenClaw deployment and an external A2A interoperability boundary, highlighting secure delegation through a controlled relay." style="display: block;" width="1227" height="422" loading="lazy">

<p>This diagram shows two separate trust boundaries.</p>
<p>On the left is your <strong>private OpenClaw deployment</strong>. This includes your Gateway, your sessions, your workspace, and any credentials or tools your agent can access. This boundary is designed for a single trusted operator.</p>
<p>On the right is the <strong>external A2A ecosystem</strong>, where remote agents live. These agents may belong to other teams or organizations and operate under different security assumptions.</p>
<p>The key idea is that communication between these two sides should happen through a <strong>controlled relay layer</strong>, not by directly exposing your OpenClaw Gateway. The relay enforces allowlists, limits what data is sent out, and ensures that remote agents cannot directly access your local tools or state.</p>
<p>This separation lets you experiment with agent interoperability while keeping your personal assistant environment safe.</p>
<p>In plain English, keep your personal assistant boundary private.</p>
<p>If you experiment with A2A, treat that as a <strong>separate exposure boundary</strong> with its own allowlists, auth, and operational controls.</p>
<p>That is why the proof-of-concept relay in this article starts with an explicit remote allowlist.</p>
<h3 id="heading-why-this-design-and-not-the-other-one">Why This Design and Not the Other One?</h3>
<p>A natural question is why this article proposes an <strong>outbound-only A2A bridge first</strong>, instead of immediately building a full bidirectional or server-style integration.</p>
<p>The short answer is that OpenClaw’s current design is centered around a <strong>personal assistant trust boundary</strong>, where one operator controls the Gateway, sessions, and tools. Introducing external agents into that environment requires careful control over what is exposed.</p>
<p>Starting with outbound delegation gives you a safer and more incremental path.</p>
<p>Outbound-only first means:</p>
<ul>
<li><p>preserving the personal-assistant trust boundary, so your local OpenClaw deployment remains private and operator-controlled</p>
</li>
<li><p>avoiding exposing the OpenClaw Gateway as a public A2A server before you have strong auth, policy, and monitoring in place</p>
</li>
<li><p>allowing you to test remote delegation patterns (Agent Cards, tasks, artifacts) without committing to full interoperability complexity</p>
</li>
<li><p>keeping OpenClaw as the user-facing control plane, while remote agents act as optional specialists</p>
</li>
</ul>
<p>This approach follows a common systems design pattern: start with <strong>controlled outbound integration</strong>, validate behavior and constraints, and only then consider expanding to inbound or bidirectional communication.</p>
<p>In practice, this means you can experiment with A2A safely, learn how the models fit together, and evolve the system without introducing unnecessary risk early on.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>OpenClaw is worth learning because it gives you a self-hosted assistant that can live in the communication tools you already use.</p>
<p>The simplest beginner path is still the right one:</p>
<ol>
<li><p>install it,</p>
</li>
<li><p>run onboarding,</p>
</li>
<li><p>check the Gateway,</p>
</li>
<li><p>open the dashboard,</p>
</li>
<li><p>try one private workflow.</p>
</li>
</ol>
<p>That is already a real end-to-end setup.</p>
<p>A2A belongs in the conversation because it gives you a credible way to connect OpenClaw to remote specialist agents later.</p>
<p>But the most important thing in this article isn't the buzzword. It's the boundary design.</p>
<p>If you keep OpenClaw as the private user-facing edge and use a narrow plugin bridge for outbound delegation, the OpenClaw session model and the A2A task model can fit together cleanly.</p>
<p>That is the architectural idea I wanted to make concrete here.</p>
<h3 id="heading-diagram-attribution">Diagram Attribution</h3>
<p>All diagrams in this article were created by the author specifically for this guide.</p>
<h2 id="heading-further-reading">Further Reading</h2>
<ul>
<li><p><a href="https://docs.openclaw.ai/">OpenClaw docs home</a></p>
</li>
<li><p><a href="https://docs.openclaw.ai/start/getting-started">OpenClaw Getting Started</a></p>
</li>
<li><p><a href="https://docs.openclaw.ai/start/wizard">OpenClaw Onboarding Wizard</a></p>
</li>
<li><p><a href="https://docs.openclaw.ai/concepts/multi-agent/">OpenClaw Multi-Agent Routing</a></p>
</li>
<li><p><a href="https://docs.openclaw.ai/concepts/session-tool/">OpenClaw Session Tools</a></p>
</li>
<li><p><a href="https://docs.openclaw.ai/tools/plugin/">OpenClaw Plugin System</a></p>
</li>
<li><p><a href="https://docs.openclaw.ai/plugins/agent-tools">OpenClaw Plugin Agent Tools</a></p>
</li>
<li><p><a href="https://docs.openclaw.ai/cli/acp">OpenClaw ACP bridge</a></p>
</li>
<li><p><a href="https://docs.openclaw.ai/gateway/security">OpenClaw Security</a></p>
</li>
<li><p><a href="https://a2a-protocol.org/latest/specification/">A2A specification</a></p>
</li>
<li><p><a href="https://a2a-protocol.org/latest/topics/agent-discovery/">A2A Agent Discovery</a></p>
</li>
<li><p><a href="https://a2a-protocol.org/latest/topics/a2a-and-mcp/">A2A and MCP</a></p>
</li>
<li><p><a href="https://a2a-protocol.org/latest/definitions/">A2A protocol definition and schema</a></p>
</li>
<li><p><a href="https://a2a-protocol.org/latest/announcing-1.0/">A2A version 1.0 announcement</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build AI Agents That Remember User Preferences (Without Breaking Context) ]]>
                </title>
                <description>
                    <![CDATA[ Why Personalization Breaks Most AI Agents Personalization is one of the most requested features in AI-powered applications. Users expect an agent to remember their preferences, adapt to their style, and improve over time. In practice, personalization... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-ai-agents-that-remember-user-preferences-without-breaking-context/</link>
                <guid isPermaLink="false">698cc32db8fec0245bd9996d</guid>
                
                    <category>
                        <![CDATA[ ai agents ]]>
                    </category>
                
                    <category>
                        <![CDATA[ llm ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ System Design ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software architecture ]]>
                    </category>
                
                    <category>
                        <![CDATA[ observability ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Machine Learning ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Developer Tools ]]>
                    </category>
                
                    <category>
                        <![CDATA[ tools ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nataraj Sundar ]]>
                </dc:creator>
                <pubDate>Wed, 11 Feb 2026 17:58:05 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770832641633/da49bdca-617e-4272-b5b7-012f3c6c1d61.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="heading-why-personalization-breaks-most-ai-agents"><strong>Why Personalization Breaks Most AI Agents</strong></h2>
<p>Personalization is one of the most requested features in AI-powered applications. Users expect an agent to remember their preferences, adapt to their style, and improve over time.</p>
<p>In practice, personalization is unfortunately also one of the fastest ways to break an otherwise working AI agent.</p>
<p>Many agents start with a simple idea: keep adding more conversation history to the prompt. This approach works for demos, but it quickly fails in real applications. Context windows grow too large. Irrelevant information leaks into decisions. Costs increase. Debugging becomes nearly impossible.</p>
<p>If you want a personalized agent that survives production, you need more than a large language model. You need a way to connect the agent to tools, manage multi-step workflows, and store user preferences safely over time – without turning your system into a tangled mess of prompts and callbacks.</p>
<p>In this tutorial, you’ll learn how to design a personalized AI agent using three core building blocks:</p>
<ul>
<li><p><strong>Agent Development Kit (ADK)</strong> to orchestrate agent reasoning and execution</p>
</li>
<li><p><strong>Model Context Protocol (MCP)</strong> to connect tools with clear boundaries</p>
</li>
<li><p><strong>Long-term memory</strong> to store preferences without polluting context</p>
</li>
</ul>
<p>Rather than focusing on setup commands or vendor-specific walkthroughs, we'll focus on the architectural patterns that make personalized agents reliable, debuggable, and maintainable.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770578645884/2fd77443-31d5-4db3-98f0-bba685122a6f.png" alt="User preferences influence an AI agent’s personalized response" class="image--center mx-auto" width="1452" height="578" loading="lazy"></p>
<p><em>Figure 1 — Personalization influences agent responses</em></p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#what-personalized-means-in-a-real-ai-agent">What “Personalized” Means in a Real AI Agent</a></p>
</li>
<li><p><a class="post-section-overview" href="#how-the-agent-architecture-fits-together">How the Agent Architecture Fits Together</a></p>
</li>
<li><p><a class="post-section-overview" href="#how-to-design-the-agent-core-with-adk">How to Design the Agent Core with ADK</a></p>
</li>
<li><p><a class="post-section-overview" href="#how-to-connect-tools-safely-with-mcp">How to Connect Tools Safely with MCP</a></p>
</li>
<li><p><a class="post-section-overview" href="#how-to-add-long-term-memory-without-polluting-context">How to Add Long-Term Memory Without Polluting Context</a></p>
<ul>
<li><a class="post-section-overview" href="#privacy-consent-and-lifecycle-controls-production-checklist">Privacy, Consent, and Lifecycle Controls (Production Checklist)</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#how-the-end-to-end-agent-flow-works">How the End-to-End Agent Flow Works</a></p>
</li>
<li><p><a class="post-section-overview" href="#common-pitfalls-youll-hit-and-how-to-avoid-them">Common Pitfalls You’ll Hit (and How to Avoid Them)</a></p>
</li>
<li><p><a class="post-section-overview" href="#what-you-learned-and-where-to-go-next">What You Learned and Where to Go Next</a></p>
</li>
</ul>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<p>To follow along with this tutorial, you should have:</p>
<ul>
<li><p>Basic familiarity with Python</p>
</li>
<li><p>A general understanding of how large language models work</p>
</li>
<li><p>Optional: a Google Cloud account if you want to run an end-to-end demo. Otherwise, you can follow the architecture and code patterns locally with stubs. We’ll avoid deep infrastructure setup and focus on design patterns rather than deployment mechanics.</p>
</li>
</ul>
<p>You don’t need prior experience with ADK or MCP. I’ll introduce each concept as it appears.</p>
<h2 id="heading-what-personalized-means-in-a-real-ai-agent"><strong>What “Personalized” Means in a Real AI Agent</strong></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770578714303/4d25a7e4-fcdd-4a1a-a12c-411e41f2021f.png" alt="An AI agent accesses external tools through a protocol boundary/control layer" class="image--center mx-auto" width="1382" height="670" loading="lazy"></p>
<p><em>Figure 2 — Keep preferences out of the prompt: agent ↔ tools across a protocol boundary</em></p>
<p>Before writing any code, it’s important to define what personalization means in an AI agent.</p>
<p>Personalization is not the same as “remembering everything.” In practice, agent state usually falls into three categories:</p>
<ol>
<li><p><strong>Short-term context:</strong> Information needed to complete the current task. This belongs in the prompt.</p>
</li>
<li><p><strong>Session state:</strong> Temporary decisions or selections made during a workflow. This should be structured and scoped to a session.</p>
</li>
<li><p><strong>Long-term memory:</strong> Durable user preferences or facts that should persist across sessions.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770577191953/3df5aa02-2eb9-4214-bbef-52f18ddb353a.png" alt="Three panels comparing short-term context, session state, and long-term memory" class="image--center mx-auto" width="946" height="510" loading="lazy"></p>
<p><em>Figure 3 — Three kinds of agent state: context (now), session (today), memory (always)</em></p>
<p>Most problems happen when these categories are mixed together.</p>
<p>If you store long-term preferences directly in the prompt, the agent’s behavior becomes unpredictable. If you store everything permanently, memory grows without bounds. If you don’t scope memory at all, unrelated sessions start influencing each other.</p>
<p>A well-designed, personalized agent treats memory as a first-class system component, not as extra text added to a prompt.</p>
<p>In the next section, we'll look at how to structure the agent so these concerns stay separated. </p>
<p>By the end of this tutorial, you’ll understand how to design a personalized AI agent that uses long-term memory safely, connects to tools through clear boundaries, and remains debuggable as it grows.</p>
<h2 id="heading-how-the-agent-architecture-fits-together"><strong>How the Agent Architecture Fits Together</strong></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770577351960/9b14cadf-d650-4098-8ce1-9fd706537bb9.png" alt="Reference architecture showing a user, an AI agent core, tools, a memory service, and an orchestration runtime" class="image--center mx-auto" width="1100" height="554" loading="lazy"></p>
<p><em>Figure 4 — Reference architecture: agent core + tools + memory service + orchestration runtime</em></p>
<p>The above diagram shows a high-level, personalized AI agent architecture. In it, an agent core handles reasoning and planning while interacting with a tool interface layer, a long-term memory service, and an orchestration runtime.</p>
<p>Let’s now understand the moving parts of a personalized agent and how they interact.</p>
<p>At a high level, the system has four responsibilities:</p>
<ol>
<li><p><strong>Reasoning</strong> – deciding what to do next</p>
</li>
<li><p><strong>Execution</strong> – calling tools and services</p>
</li>
<li><p><strong>Memory</strong> – storing and retrieving long-term preferences</p>
</li>
<li><p><strong>Boundaries</strong> – controlling what the agent is allowed to do</p>
</li>
</ol>
<p>A common mistake you’ll see is to blur these responsibilities together. For example, letting the model decide when to write memory, or allowing tools to execute actions without clear constraints.</p>
<p>Instead, you'll design the system so each responsibility has a clear owner. The core components look like this:</p>
<ul>
<li><p><strong>Agent core</strong>: Handles reasoning and planning</p>
</li>
<li><p><strong>Tools</strong>: Perform external actions (read or write)</p>
</li>
<li><p><strong>MCP layer</strong>: Defines how tools are exposed and invoked</p>
</li>
<li><p><strong>Memory services</strong>: Store long-term user data safely</p>
</li>
</ul>
<p>ADK sits at the center, orchestrating how requests flow between these components. The model never directly talks to databases or services. It reasons about actions, and ADK coordinates execution.</p>
<p>This separation makes the system easier to reason about, debug, and extend.</p>
<h2 id="heading-how-to-design-the-agent-core-with-adk"><strong>How to Design the Agent Core with ADK</strong></h2>
<p>Before we dive in, a quick note on what ADK is<strong>.</strong>  </p>
<p><strong>Agent Development Kit (ADK)</strong> is an agent orchestration framework – the glue code between a large language model and your application. Instead of treating the model as a black box that directly “does things”, ADK helps you structure the agent as a system:</p>
<ul>
<li><p>The model focuses on <strong>reasoning</strong> (turning user intent, context, and memory into a structured plan)</p>
</li>
<li><p>Your runtime stays in control of <strong>execution</strong> (deciding which tools can run, how they run, and what gets logged or persisted)</p>
</li>
</ul>
<p>In other words, ADK is what lets you take tool calling and multi-step workflows out of a giant prompt and turn them into a maintainable and testable architecture. In this tutorial, we’ll use ADK to refer to that orchestration layer. The same patterns apply if you use a different agent framework.</p>
<p><strong>Note:</strong> The following code snippets are simplified reference examples intended to illustrate architectural patterns. They’re not production-ready drop-ins.</p>
<p>Once you understand the architecture, you can start designing the agent core. The agent core is responsible for reasoning, not execution.</p>
<p>A helpful mental model is to think of the agent as a planner, not a doer. Its role is to interpret the user’s goal, consider available context and memory, and produce a structured plan that can later be executed in a controlled way.</p>
<p>To make this concrete, the following example shows how an agent can translate user input and memory into an explicit plan. In practice, ADK orchestrates this using a large language model, but the important idea is that the output is structured intent, not side effects.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Reference example for illustration.</span>

<span class="hljs-keyword">from</span> dataclasses <span class="hljs-keyword">import</span> dataclass
<span class="hljs-keyword">from</span> typing <span class="hljs-keyword">import</span> List, Dict, Any

<span class="hljs-meta">@dataclass</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Step</span>:</span>
    tool: str
    args: Dict[str, Any]

<span class="hljs-meta">@dataclass</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Plan</span>:</span>
    goal: str
    steps: List[Step]

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">build_plan</span>(<span class="hljs-params">user_text: str, memory: Dict[str, Any]</span>) -&gt; Plan:</span>
    <span class="hljs-comment"># In practice, the LLM produces this structure via ADK orchestration.</span>
    goal = <span class="hljs-string">f"Help user: <span class="hljs-subst">{user_text}</span>"</span>
    steps = []
    <span class="hljs-keyword">if</span> memory.get(<span class="hljs-string">"prefers_short_answers"</span>):
        steps.append(Step(tool=<span class="hljs-string">"set_style"</span>, args={<span class="hljs-string">"verbosity"</span>: <span class="hljs-string">"low"</span>}))
    steps.append(Step(tool=<span class="hljs-string">"search_docs"</span>, args={<span class="hljs-string">"query"</span>: user_text}))
    steps.append(Step(tool=<span class="hljs-string">"summarize"</span>, args={<span class="hljs-string">"max_bullets"</span>: <span class="hljs-number">5</span>}))
    <span class="hljs-keyword">return</span> Plan(goal=goal, steps=steps)
</code></pre>
<p>This example illustrates an important constraint: the agent produces a plan, but it doesn’t execute anything directly.</p>
<p>The agent decides <em>what</em> should happen and <em>in what order</em>, while ADK controls <em>when</em> and <em>how</em> each step runs. This separation lets you inspect, test, and reason about decisions before they result in real-world actions.</p>
<p>When personalization is involved, this distinction becomes critical. Preferences may influence planning, but execution should remain tightly controlled by the runtime.</p>
<p>Again, we can consider the agent to be a planner, not a doer.</p>
<p>It should not:</p>
<ul>
<li><p>Perform side effects directly</p>
</li>
<li><p>Write to databases</p>
</li>
<li><p>Call external APIs without supervision</p>
</li>
</ul>
<p>In ADK, this separation is natural. The agent produces intents and tool calls, while the runtime controls how and when those calls are executed.</p>
<p>This design has two major benefits:</p>
<ol>
<li><p><strong>Safety</strong> – you can restrict which tools the agent can access</p>
</li>
<li><p><strong>Debuggability</strong> – you can inspect decisions before execution</p>
</li>
</ol>
<p>When personalization is involved, this becomes even more important. Preferences influence reasoning, but execution should remain tightly controlled.</p>
<h2 id="heading-how-to-connect-tools-safely-with-mcp"><strong>How to Connect Tools Safely with MCP</strong></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770578793149/2e3f8282-341a-4f03-9313-df3f8c9c5174.png" alt="Tool call routed through a control layer with request, validation, execution, and response steps." class="image--center mx-auto" width="1362" height="870" loading="lazy"></p>
<p><em>Figure 5 — Tool calls with guardrails: request → validate → execute → respond</em></p>
<p>Tools are how agents interact with the real world. They fetch data, generate artifacts, and sometimes perform actions with side effects.</p>
<p>Without clear boundaries, tool usage quickly becomes a source of fragility. Hardcoded API calls leak into prompts, tools evolve independently, and agents gain more authority than intended.</p>
<p>To avoid these problems, tools should be explicitly registered and invoked through a narrow interface. The following example shows a simple tool registry pattern that mirrors how MCP exposes tools to an agent without tightly coupling it to implementations.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Reference example (pseudocode for illustration)</span>

<span class="hljs-keyword">from</span> typing <span class="hljs-keyword">import</span> Callable, Dict, Any

ToolFn = Callable[[Dict[str, Any]], Dict[str, Any]]

TOOLS: Dict[str, ToolFn] = {}

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">register_tool</span>(<span class="hljs-params">name: str</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">decorator</span>(<span class="hljs-params">fn: ToolFn</span>):</span>
        TOOLS[name] = fn
        <span class="hljs-keyword">return</span> fn
    <span class="hljs-keyword">return</span> decorator

<span class="hljs-meta">@register_tool("search_docs")</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">search_docs</span>(<span class="hljs-params">args: Dict[str, Any]</span>) -&gt; Dict[str, Any]:</span>
    query = args[<span class="hljs-string">"query"</span>]
    <span class="hljs-comment"># Replace with your MCP client call (or local tool implementation).</span>
    <span class="hljs-keyword">return</span> {<span class="hljs-string">"results"</span>: [<span class="hljs-string">f"doc://example?q=<span class="hljs-subst">{query}</span>"</span>]}

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">invoke_tool</span>(<span class="hljs-params">name: str, args: Dict[str, Any]</span>) -&gt; Dict[str, Any]:</span>
    <span class="hljs-keyword">if</span> name <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> TOOLS:
        <span class="hljs-keyword">raise</span> ValueError(<span class="hljs-string">f"Tool not allowed: <span class="hljs-subst">{name}</span>"</span>)
    <span class="hljs-keyword">return</span> TOOLS[name](args)
</code></pre>
<p>The Model Context Protocol (MCP) provides a clean way to formalize this pattern. You can think of MCP the same way operating systems treat system calls.</p>
<p>An application does not directly manipulate hardware. Instead, it requests operations through well-defined system calls. The kernel decides whether the operation is allowed and how it executes.</p>
<p>In the same way, the agent knows <em>what</em> capabilities exist, MCP defines <em>how</em> those capabilities are invoked, and the runtime controls <em>when</em> and <em>whether</em> they execute.</p>
<p>This separation prevents several common problems, including hardcoded API details in prompts, unexpected breakage when tools change, and agents performing unrestricted side effects.</p>
<p>When designing tools, it helps to classify them by risk: read tools for safe queries, generate tools for planning or synthesis, and commit tools for irreversible actions. In a personalized agent, commit tools should be rare and tightly guarded.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770580271505/d5d34514-3b98-4997-85ed-dee55e65d711.png" alt="Observability around tool calls using logs, traces, and timing across decision points" class="image--center mx-auto" width="996" height="606" loading="lazy"></p>
<p><em>Figure 6 — Observability around tool calls: logs, traces, timing, decision points</em></p>
<h2 id="heading-how-to-add-long-term-memory-without-polluting-context"><strong>How to Add Long-Term Memory Without Polluting Context</strong></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770577944241/b2a3de65-c5e2-456e-8a33-e9fd4d2695f0.png" alt="Memory candidates extracted from user input, filtered and validated, then stored asynchronously" class="image--center mx-auto" width="1118" height="478" loading="lazy"></p>
<p><em>Figure 7 — Memory admission pipeline: extract → filter/validate → persist asynchronously</em></p>
<p>Memory is where personalization either succeeds or fails.</p>
<p>You can start by storing everything the user says and feed it back into the prompt. This works briefly, then collapses under its own weight as context grows, costs rise, and behavior becomes unpredictable.</p>
<p>A better approach is to treat memory as structured, curated data so you can control what the agent remembers and why with clear admission rules. Before persisting anything, the system should explicitly decide whether the information is worth remembering. The following function demonstrates a simple memory admission policy.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Simplified Reference Only</span>
<span class="hljs-keyword">from</span> typing <span class="hljs-keyword">import</span> Optional, Dict, Any

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">memory_candidate</span>(<span class="hljs-params">user_text: str</span>) -&gt; Optional[Dict[str, Any]]:</span>
    text = user_text.lower()

    <span class="hljs-comment"># Durable</span>
    <span class="hljs-keyword">if</span> <span class="hljs-string">"for this session"</span> <span class="hljs-keyword">in</span> text <span class="hljs-keyword">or</span> <span class="hljs-string">"ignore after"</span> <span class="hljs-keyword">in</span> text:
        <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span>

    <span class="hljs-comment"># Reusable</span>
    <span class="hljs-keyword">if</span> <span class="hljs-string">"my preferred language is"</span> <span class="hljs-keyword">in</span> text:
        <span class="hljs-keyword">return</span> {<span class="hljs-string">"type"</span>: <span class="hljs-string">"preference"</span>, <span class="hljs-string">"key"</span>: <span class="hljs-string">"language"</span>, <span class="hljs-string">"value"</span>: user_text.split()[<span class="hljs-number">-1</span>]}

    <span class="hljs-comment"># Safe (basic example; add PII checks for your use case)</span>
    <span class="hljs-keyword">if</span> <span class="hljs-string">"password"</span> <span class="hljs-keyword">in</span> text <span class="hljs-keyword">or</span> <span class="hljs-string">"ssn"</span> <span class="hljs-keyword">in</span> text:
        <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span>

    <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span>  <span class="hljs-comment"># default: don’t store</span>
</code></pre>
<p>This policy encodes three questions every memory candidate must answer:</p>
<ul>
<li><p>Is it durable? Will it still matter in the future?</p>
</li>
<li><p>Is it reusable? Will it influence future decisions meaningfully?</p>
</li>
<li><p>Is it safe to persist? Does it avoid sensitive or session-specific data?</p>
</li>
</ul>
<p>Only information that passes all three checks should become long-term memory. In practice, this usually includes stable preferences and long-lived constraints, not temporary instructions or intermediate reasoning.</p>
<h3 id="heading-privacy-consent-and-lifecycle-controls-production-checklist"><strong>Privacy, Consent, and Lifecycle Controls (Production Checklist)</strong></h3>
<p>Even if your admission rules are solid, long-term memory introduces governance requirements:</p>
<ul>
<li><p><strong>User control:</strong> allow users to view, export, and delete stored preferences at any time.</p>
</li>
<li><p><strong>Sensitive data handling:</strong> never store secrets/PII. Run PII detection on every memory candidate (and consider redaction).</p>
</li>
<li><p><strong>Retention + consent:</strong> use explicit consent for persistent memory and apply retention windows (TTL) so memory expires unless it’s still useful.</p>
</li>
<li><p><strong>Security + auditability:</strong> encrypt at rest, restrict access by service identity, and keep an audit log of memory writes/updates.</p>
</li>
</ul>
<p>Memory writes should also be asynchronous. The agent should never block while persisting memory, which keeps interactions responsive and avoids coupling reasoning to storage latency.</p>
<h2 id="heading-how-the-end-to-end-agent-flow-works"><strong>How the End-to-End Agent Flow Works</strong></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770578847727/f3cbc4b9-5bc9-4026-ae69-6fd7bc1625fc.png" alt="End-to-end flow showing user input, agent reasoning, tool invocation, and memory updates with feedback loops" class="image--center mx-auto" width="1134" height="308" loading="lazy"></p>
<p><em>Figure 8 — End-to-end request lifecycle: user input → plan → tools → memory updates</em></p>
<p>At this point, you can trace exactly how memory and tools interact during a single request. With the individual components in place, it’s helpful to see how they work together during a single request. The following example walks through the full lifecycle of a personalized interaction, from user input to response.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Reference example (pseudocode for illustration)</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_request</span>(<span class="hljs-params">user_id: str, user_text: str</span>) -&gt; str:</span>
    memory = memory_store.get(user_id)  <span class="hljs-comment"># e.g., {"prefers_short_answers": True}</span>
    plan = build_plan(user_text, memory)

    tool_outputs = []
    <span class="hljs-keyword">for</span> step <span class="hljs-keyword">in</span> plan.steps:
        out = invoke_tool(step.tool, step.args)
        tool_outputs.append({step.tool: out})

    response = render_response(goal=plan.goal, tool_outputs=tool_outputs, memory=memory)

    cand = memory_candidate(user_text)
    <span class="hljs-keyword">if</span> cand:
        <span class="hljs-comment"># Never block the user on storage.</span>
        memory_store.write_async(user_id, cand)
    <span class="hljs-keyword">return</span> response
</code></pre>
<p>At a high level, the flow looks like this:</p>
<ol>
<li><p>The user sends a message.</p>
</li>
<li><p>Relevant long-term memory is retrieved.</p>
</li>
<li><p>The agent reasons about the request and produces a plan.</p>
</li>
<li><p>ADK invokes tools through MCP as needed.</p>
</li>
<li><p>Results flow back to the agent.</p>
</li>
<li><p>The agent decides whether new information should be persisted.</p>
</li>
<li><p>Memory is written asynchronously.</p>
</li>
<li><p>The final response is returned to the user.</p>
</li>
</ol>
<p>Notice what does <strong>not</strong> happen: the model does not directly write memory, tools do not execute without coordination, and context does not grow without bounds. This structure keeps personalization controlled and predictable.</p>
<h2 id="heading-common-pitfalls-youll-hit-and-how-to-avoid-them"><strong>Common Pitfalls You’ll Hit (and How to Avoid Them)</strong></h2>
<p>Even with a solid architecture, there are a few failure modes that show up repeatedly in real systems. Many of them stem from allowing agents to perform irreversible actions without explicit checks.</p>
<p>The following example shows a simple guardrail for commit-style tools that require approval before execution.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Reference example (pseudocode for illustration)</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">invoke_commit_tool</span>(<span class="hljs-params">name: str, args: Dict[str, Any], approved: bool</span>) -&gt; Dict[str, Any]:</span>
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> approved:
        <span class="hljs-comment"># Require explicit confirmation or policy approval before side effects.</span>
        <span class="hljs-keyword">return</span> {<span class="hljs-string">"status"</span>: <span class="hljs-string">"blocked"</span>, <span class="hljs-string">"reason"</span>: <span class="hljs-string">"commit tools require approval"</span>}

    <span class="hljs-comment"># For example: create_ticket, send_email, submit_order, update_record</span>
    <span class="hljs-keyword">return</span> invoke_tool(name, args)
</code></pre>
<p>This pattern forces a clear decision point before side effects occur. It also creates an audit trail that explains <em>why</em> an action was allowed or blocked.</p>
<p>Other common pitfalls include over-personalization, leaky memory that persists session-specific data, uncontrolled tool growth, and debugging blind spots caused by unclear boundaries. If you see these symptoms, it usually means responsibilities are not clearly separated.</p>
<h2 id="heading-what-you-learned-and-where-to-go-next"><strong>What You Learned and Where to Go Next</strong></h2>
<p>Personalized AI agents are powerful, but they require discipline. The key insight is that personalization is a <strong>systems problem</strong>, not a prompt problem.</p>
<p>By separating reasoning from execution, structuring memory carefully, and using protocols like MCP to enforce boundaries, you can build agents that scale beyond demos and remain maintainable in production.</p>
<p>As you extend this system, resist the urge to add “just one more prompt tweak.” Instead, ask whether the change belongs in memory, tools, or orchestration.  </p>
<p>That mindset will save you time as your agent grows in complexity.  </p>
<p>If you’d like to continue the conversation, you can find me on <a target="_blank" href="https://www.linkedin.com/in/natarajsundar/">LinkedIn</a>.</p>
<p>*All diagrams in this article were created by the author for educational purposes.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use the Singleton Design Pattern in Flutter: Lazy, Eager, and Factory Variations ]]>
                </title>
                <description>
                    <![CDATA[ In software engineering, sometimes you need only one instance of a class across your entire application. Creating multiple instances in such cases can lead to inconsistent behavior, wasted memory, or resource conflicts. The Singleton Design Pattern i... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-the-singleton-design-pattern-in-flutter-lazy-eager-and-factory-variations/</link>
                <guid isPermaLink="false">69740b7bc3e68b8de44a179f</guid>
                
                    <category>
                        <![CDATA[ Singleton Design Pattern ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Object Oriented Programming ]]>
                    </category>
                
                    <category>
                        <![CDATA[ design patterns ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ood ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Dart ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software architecture ]]>
                    </category>
                
                    <category>
                        <![CDATA[ flutter development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Factory Design Pattern ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Mobile Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mobile app development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Oluwaseyi Fatunmole ]]>
                </dc:creator>
                <pubDate>Fri, 23 Jan 2026 23:59:55 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769212761076/11d41d2a-8efa-4ddb-9ee2-218f5be00d9f.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In software engineering, sometimes you need only one instance of a class across your entire application. Creating multiple instances in such cases can lead to inconsistent behavior, wasted memory, or resource conflicts.</p>
<p>The Singleton Design Pattern is a creational design pattern that solves this problem by ensuring that a class has exactly one instance and provides a global point of access to it.</p>
<p>This pattern is widely used in mobile apps, backend systems, and Flutter applications for managing shared resources such as:</p>
<ul>
<li><p>Database connections</p>
</li>
<li><p>API clients</p>
</li>
<li><p>Logging services</p>
</li>
<li><p>Application configuration</p>
</li>
<li><p>Security checks during app bootstrap</p>
</li>
</ul>
<p>In this article, we'll explore what the Singleton pattern is, how to implement it in Flutter/Dart, its variations (eager, lazy, and factory), and physical examples. By the end, you'll understand the proper way to use this pattern effectively and avoid common pitfalls.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-the-singleton-pattern">What is the Singleton Pattern?</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-when-to-use-the-singleton-pattern">When to Use the Singleton Pattern</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-a-singleton-class">How to Create a Singleton Class</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-eager-singleton">Eager Singleton</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-lazy-singleton">Lazy Singleton</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-choosing-between-eager-and-lazy">Choosing Between Eager and Lazy</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-factory-constructors-in-the-singleton-pattern">Factory Constructors in the Singleton Pattern</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-are-factory-constructors">What Are Factory Constructors?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-implementing-singleton-with-factory-constructor">Implementing Singleton with Factory Constructor</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-when-not-to-use-a-singleton">When Not to Use a Singleton</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-why-singletons-can-be-problematic">Why Singletons Can Be Problematic</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-scenarios-where-you-should-avoid-singletons">Scenarios Where You Should Avoid Singletons</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-general-guidelines">General Guidelines</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ol>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before diving into this tutorial, you should have:</p>
<ol>
<li><p>Basic understanding of the Dart programming language</p>
</li>
<li><p>Familiarity with Object-Oriented Programming (OOP) concepts, particularly classes and constructors</p>
</li>
<li><p>Basic knowledge of Flutter development (helpful but not required)</p>
</li>
<li><p>Understanding of static variables and methods in Dart</p>
</li>
<li><p>Familiarity with the concept of class instantiation</p>
</li>
</ol>
<h2 id="heading-what-is-the-singleton-pattern">What is the Singleton Pattern?</h2>
<p>The Singleton pattern is a creational design pattern that ensures a class has only one instance and that there is a global point of access to the instance.</p>
<p>Again, this is especially powerful when managing shared resources across an application.</p>
<h3 id="heading-when-to-use-the-singleton-pattern">When to Use the Singleton Pattern</h3>
<p>You should use a Singleton when you are designing parts of your system that must exist once, such as:</p>
<ol>
<li><p>Global app state (user session, auth token, app config)</p>
</li>
<li><p>Shared services (logger, API client, database connection)</p>
</li>
<li><p>Resource heavy logic (encryption handlers, ML models, cache manager)</p>
</li>
<li><p>Application boot security (run platform-specific root/jailbreak checks)</p>
</li>
</ol>
<p>For example, in a Flutter app, Android may check developer mode or root status, while iOS checks jailbroken device state. A Singleton security class is a perfect way to enforce that these checks run once globally during app startup.</p>
<h2 id="heading-how-to-create-a-singleton-class">How to Create a Singleton Class</h2>
<p>We have two major ways of creating a singleton class:</p>
<ol>
<li><p>Eager Instantiation</p>
</li>
<li><p>Lazy Instantiation</p>
</li>
</ol>
<h3 id="heading-eager-singleton">Eager Singleton</h3>
<p>This is where the Singleton is created at load time, whether it's used or not.</p>
<p>In this case, the instance of the singleton class as well as any initialization logic runs at load time, regardless of when this class is actually needed or used. Here's how it works:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">EagerSingleton</span> </span>{
  EagerSingleton._internal();
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> EagerSingleton _instance = EagerSingleton._internal();

  <span class="hljs-keyword">static</span> EagerSingleton <span class="hljs-keyword">get</span> instance =&gt; _instance;

  <span class="hljs-keyword">void</span> sayHello() =&gt; <span class="hljs-built_in">print</span>(<span class="hljs-string">"Hello from Eager Singleton"</span>);
}

<span class="hljs-comment">//usage</span>
<span class="hljs-keyword">void</span> main() {
  <span class="hljs-comment">// Accessing the singleton globally</span>
  EagerSingleton.instance.sayHello();
}
</code></pre>
<h4 id="heading-how-the-eager-singleton-works">How the Eager Singleton Works</h4>
<p>Let's break down what's happening in this implementation:</p>
<p>First, <code>EagerSingleton._internal()</code> is a private named constructor (notice the underscore prefix). This prevents external code from creating new instances using <code>EagerSingleton()</code>. The only way to get an instance is through the controlled mechanism we're about to define.</p>
<p>Next, <code>static final EagerSingleton _instance = EagerSingleton._internal();</code> is the key line. This creates the single instance immediately when the class is first loaded into memory. Because it's <code>static final</code>, it belongs to the class itself (not any particular instance) and can only be assigned once. The instance is created right here, at declaration time.</p>
<p>The <code>static EagerSingleton get instance =&gt; _instance;</code> getter provides global access to that single instance. Whenever you call <code>EagerSingleton.instance</code> anywhere in your code, you're getting the exact same object that was created when the class loaded.</p>
<p>Finally, <code>sayHello()</code> is just a regular method to demonstrate that the singleton works. You could replace this with any business logic your singleton needs to perform.</p>
<p>When you run the code in <code>main()</code>, the class loads, the instance is created immediately, and <code>EagerSingleton.instance.sayHello()</code> accesses that pre-created instance to call the method.</p>
<h4 id="heading-pros">Pros:</h4>
<ol>
<li><p>This is simple and thread safe, meaning it's not affected by concurrency, especially when your app runs on multithreads.</p>
</li>
<li><p>It's ideal if the instance is lightweight and may be accessed frequently.</p>
</li>
</ol>
<h4 id="heading-cons">Cons:</h4>
<ol>
<li>If this instance is never used through the runtime, it results in wasted memory and could impact application performance.</li>
</ol>
<h3 id="heading-lazy-singleton">Lazy Singleton</h3>
<p>In this case, the singleton instance is only created when the class is called or needed in runtime. Here, a trigger needs to happen before the instance is created. Let's see an example:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LazySingleton</span> </span>{
  LazySingleton._internal(); 
  <span class="hljs-keyword">static</span> LazySingleton? _instance;

  <span class="hljs-keyword">static</span> LazySingleton <span class="hljs-keyword">get</span> instance {
    _instance ??= LazySingleton._internal();
    <span class="hljs-keyword">return</span> _instance!;
  }

  <span class="hljs-keyword">void</span> sayHello() =&gt; <span class="hljs-built_in">print</span>(<span class="hljs-string">"Hello from LazySingleton"</span>);
}

<span class="hljs-comment">//usage </span>
<span class="hljs-keyword">void</span> main() {
  <span class="hljs-comment">// Accessing the singleton globally</span>
  LazySingleton.instance.sayHello();
}
</code></pre>
<h4 id="heading-how-the-lazy-singleton-works">How the Lazy Singleton Works</h4>
<p>The lazy implementation differs from eager in one crucial way: timing.</p>
<p>Again, <code>LazySingleton._internal()</code> is a private constructor that prevents external instantiation.</p>
<p>But notice that <code>static LazySingleton? _instance;</code> is declared as nullable and not initialized. Unlike the eager version, no instance is created at load time. The variable simply exists as <code>null</code> until it's needed.</p>
<p>The magic happens in the getter: <code>_instance ??= LazySingleton._internal();</code> uses Dart's null-aware assignment operator. This line says "if <code>_instance</code> is null, create a new instance and assign it. Otherwise, keep the existing one." This is the lazy initialization: the instance is only created the first time someone accesses it.</p>
<p>The first time you call <code>LazySingleton.instance</code>, <code>_instance</code> is null, so a new instance is created. Every subsequent call finds that <code>_instance</code> already exists, so it just returns that same instance.</p>
<p>The <code>return _instance!;</code> uses the null assertion operator because we know <code>_instance</code> will never be null at this point (we just ensured it's not null in the previous line).</p>
<p>This approach saves memory because if you never call <code>LazySingleton.instance</code> in your app, the instance never gets created.</p>
<h4 id="heading-pros-1">Pros:</h4>
<ol>
<li><p>Saves application memory, as it only creates what is needed in runtime.</p>
</li>
<li><p>Avoids memory leaks.</p>
</li>
<li><p>Is ideal for resource heavy objects while considering application performance.</p>
</li>
</ol>
<h4 id="heading-cons-1">Cons:</h4>
<ol>
<li>Could be difficult to manage in multithreaded environments, as you have to ensure thread safety while following this pattern.</li>
</ol>
<h3 id="heading-choosing-between-eager-and-lazy">Choosing Between Eager and Lazy</h3>
<p>Now that we've broken down these two major types of singleton instantiation, it's worthy of note that you'll need to be intentional while deciding whether to create a singleton the eager or lazy way. Your use case/context should help you determine what singleton pattern you need to apply during object creation.</p>
<p>As an engineer, you need to ask yourself these questions when using a singleton for object creation:</p>
<ol>
<li><p>Do I need this class instantiated when the app loads?</p>
</li>
<li><p>Based on the user journey, will this class always be needed during every session?</p>
</li>
<li><p>Can a user journey be completed without needing to call any logic in this class?</p>
</li>
</ol>
<p>These three questions will determine what pattern (eager or lazy) you should use to fulfill best practices while maintaining scalability and high performance in your application.</p>
<h2 id="heading-factory-constructors-in-the-singleton-pattern">Factory Constructors in the Singleton Pattern</h2>
<p>Applying factory constructors in the Singleton pattern can be powerful if you use them properly. But first, let's understand what factory constructors are.</p>
<h3 id="heading-what-are-factory-constructors">What Are Factory Constructors?</h3>
<p>A factory constructor in Dart is a special type of constructor that doesn't always create a new instance of its class. Unlike regular constructors that must return a new instance, factory constructors can:</p>
<ol>
<li><p>Return an existing instance (perfect for singletons)</p>
</li>
<li><p>Return a subclass instance</p>
</li>
<li><p>Apply logic before deciding what to return</p>
</li>
<li><p>Perform validation or initialization before returning an object</p>
</li>
</ol>
<p>The <code>factory</code> keyword tells Dart that this constructor has the flexibility to return any instance of the class (or its subtypes), not necessarily a fresh one.</p>
<h3 id="heading-implementing-singleton-with-factory-constructor">Implementing Singleton with Factory Constructor</h3>
<p>This allows you to apply initialization logic while your class instance is being created before returning the instance.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FactoryLazySingleton</span> </span>{
  FactoryLazySingleton._internal();
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> FactoryLazySingleton _instance = FactoryLazySingleton._internal();

  <span class="hljs-keyword">static</span> FactoryLazySingleton <span class="hljs-keyword">get</span> instance =&gt; _instance;

  <span class="hljs-keyword">factory</span> FactoryLazySingleton() {
    <span class="hljs-comment">// Your logic runs here</span>
    <span class="hljs-built_in">print</span>(<span class="hljs-string">"Factory constructor called"</span>);
    <span class="hljs-keyword">return</span> _instance;
  }
}
</code></pre>
<h4 id="heading-how-the-factory-constructor-singleton-works">How the Factory Constructor Singleton Works</h4>
<p>This implementation combines aspects of both eager and lazy patterns with additional control.</p>
<p>The <code>FactoryLazySingleton._internal()</code> private constructor and <code>static final _instance</code> create an eager singleton. The instance is created immediately when the class loads.</p>
<p>The <code>static get instance</code> provides the traditional singleton access pattern we've seen before.</p>
<p>But the interesting part is the <code>factory FactoryLazySingleton()</code> constructor. This is a public constructor that looks like a normal constructor call, but behaves differently. When you call <code>FactoryLazySingleton()</code>, instead of creating a new instance, it runs whatever logic you've placed inside (in this case, a print statement), then returns the existing <code>_instance</code>.</p>
<p>This pattern is powerful because:</p>
<ol>
<li><p>You can log when someone tries to create an instance</p>
</li>
<li><p>You can validate conditions before returning the instance</p>
</li>
<li><p>You can apply configuration based on parameters passed to the factory</p>
</li>
<li><p>You can choose to return different singleton instances based on conditions</p>
</li>
</ol>
<p>For example, you might have different configuration singletons for development vs production:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">factory</span> FactoryLazySingleton({<span class="hljs-built_in">bool</span> isProduction = <span class="hljs-keyword">false</span>}) {
  <span class="hljs-keyword">if</span> (isProduction) {
    <span class="hljs-comment">// Apply production configuration</span>
    _instance.configure(productionSettings);
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">// Apply development configuration</span>
    _instance.configure(devSettings);
  }
  <span class="hljs-keyword">return</span> _instance;
}
</code></pre>
<h4 id="heading-pros-2">Pros</h4>
<ol>
<li><p>You can add logic before returning an instance</p>
</li>
<li><p>You can cache or reuse the same object</p>
</li>
<li><p>You can dynamically return a subtype if needed</p>
</li>
<li><p>You avoid unnecessary instantiation</p>
</li>
<li><p>You can inject configuration or environment logic</p>
</li>
</ol>
<h4 id="heading-cons-2">Cons</h4>
<ol>
<li><p>Adds slight complexity compared to simple getter access</p>
</li>
<li><p>The factory constructor syntax might confuse developers unfamiliar with the pattern</p>
</li>
<li><p>If overused with complex logic, it can make debugging harder</p>
</li>
<li><p>Can create misleading code where <code>FactoryLazySingleton()</code> looks like it creates a new instance but doesn't</p>
</li>
</ol>
<h2 id="heading-when-not-to-use-a-singleton">When Not to Use a Singleton</h2>
<p>While singletons are powerful, they're not always the right solution. Understanding when to avoid them is just as important as knowing when to use them.</p>
<h3 id="heading-why-singletons-can-be-problematic">Why Singletons Can Be Problematic</h3>
<p>Singletons create global state, which can make your application harder to reason about and test. They introduce tight coupling between components that shouldn't necessarily know about each other, and they can make it difficult to isolate components for unit testing.</p>
<h3 id="heading-scenarios-where-you-should-avoid-singletons">Scenarios Where You Should Avoid Singletons</h3>
<p>Avoid using the Singleton pattern if:</p>
<h4 id="heading-you-need-multiple-independent-instances">You need multiple independent instances</h4>
<p>If different parts of your app need their own separate configurations or states, singletons force you into a one-size-fits-all approach.</p>
<p>For example, if you're building a multi-tenant application where each tenant needs isolated data, a singleton would cause data to bleed between tenants.</p>
<p><strong>Alternative</strong>: Use dependency injection to pass different instances to different parts of your app. Each component receives the specific instance it needs through its constructor or a service locator.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// Instead of singleton</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserRepository</span> </span>{
  <span class="hljs-keyword">final</span> DatabaseConnection db;
  UserRepository(<span class="hljs-keyword">this</span>.db); 
}

<span class="hljs-comment">// Usage</span>
<span class="hljs-keyword">final</span> dbForTenantA = DatabaseConnection(tenantId: <span class="hljs-string">'A'</span>);
<span class="hljs-keyword">final</span> dbForTenantB = DatabaseConnection(tenantId: <span class="hljs-string">'B'</span>);
<span class="hljs-keyword">final</span> repoA = UserRepository(dbForTenantA);
<span class="hljs-keyword">final</span> repoB = UserRepository(dbForTenantB);
</code></pre>
<h4 id="heading-your-architecture-avoids-shared-global-state">Your architecture avoids shared global state</h4>
<p>Modern architectural patterns like BLoC, Provider, or Riverpod in Flutter specifically aim to avoid global mutable state. Singletons work against these patterns by reintroducing global state.</p>
<p><strong>Alternative</strong>: Use state management solutions designed for Flutter. Provider, Riverpod, BLoC, or GetX offer better ways to share data across your app while maintaining testability and avoiding tight coupling.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// Using Provider instead of singleton</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppConfig</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> apiUrl;
  AppConfig(<span class="hljs-keyword">this</span>.apiUrl);
}

<span class="hljs-comment">// Provide it at the top level</span>
<span class="hljs-keyword">void</span> main() {
  runApp(
    Provider&lt;AppConfig&gt;(
      create: (_) =&gt; AppConfig(<span class="hljs-string">'https://api.example.com'</span>),
      child: MyApp(),
    ),
  );
}

<span class="hljs-comment">// Access it anywhere in the widget tree</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyWidget</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">final</span> config = Provider.of&lt;AppConfig&gt;(context);

  }
}
</code></pre>
<h4 id="heading-it-forces-tight-coupling-between-unrelated-classes">It forces tight coupling between unrelated classes</h4>
<p>When multiple unrelated classes depend on the same singleton, they become indirectly coupled. Changes to the singleton affect all these classes, making the codebase fragile and hard to refactor.</p>
<p><strong>Alternative</strong>: Use interfaces and dependency injection. Define what behavior you need through an interface, then inject implementations. This way, classes depend on abstractions, not concrete singletons.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// Define an interface</span>
<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Logger</span> </span>{
  <span class="hljs-keyword">void</span> log(<span class="hljs-built_in">String</span> message);
}

<span class="hljs-comment">// Implementation</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ConsoleLogger</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Logger</span> </span>{
  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> log(<span class="hljs-built_in">String</span> message) =&gt; <span class="hljs-built_in">print</span>(message);
}

<span class="hljs-comment">// Classes depend on the interface, not a singleton</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PaymentService</span> </span>{
  <span class="hljs-keyword">final</span> Logger logger;
  PaymentService(<span class="hljs-keyword">this</span>.logger);

  <span class="hljs-keyword">void</span> processPayment() {
    logger.log(<span class="hljs-string">'Processing payment'</span>);
  }
}

<span class="hljs-comment">// Easy to test with mock</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MockLogger</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Logger</span> </span>{
  <span class="hljs-built_in">List</span>&lt;<span class="hljs-built_in">String</span>&gt; logs = [];
  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> log(<span class="hljs-built_in">String</span> message) =&gt; logs.add(message);
}
</code></pre>
<h4 id="heading-you-need-clean-isolated-testing">You need clean, isolated testing</h4>
<p>Singletons maintain state between tests, causing test pollution where one test affects another. This makes tests unreliable and order-dependent.</p>
<p><strong>Alternative</strong>: Use dependency injection and create fresh instances for each test. Most testing frameworks support this pattern, allowing you to inject mocks or fakes easily.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// Testable code</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderService</span> </span>{
  <span class="hljs-keyword">final</span> PaymentProcessor processor;
  OrderService(<span class="hljs-keyword">this</span>.processor);
}

<span class="hljs-comment">// In tests</span>
<span class="hljs-keyword">void</span> main() {
  test(<span class="hljs-string">'processes order successfully'</span>, () {
    <span class="hljs-keyword">final</span> mockProcessor = MockPaymentProcessor();
    <span class="hljs-keyword">final</span> service = OrderService(mockProcessor); 

  });
}
</code></pre>
<h3 id="heading-general-guidelines">General Guidelines</h3>
<p>Use singletons sparingly and only when you truly need exactly one instance of something for the entire application lifecycle. Good candidates include logging systems, application-level configuration, and hardware interface managers.</p>
<p>For most other cases, prefer dependency injection, state management solutions, or simply passing instances where needed. These approaches make your code more flexible, testable, and maintainable.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>The Singleton pattern is a powerful creational tool, but like every tool, you should use it strategically.</p>
<p>Overusing singletons can make apps tightly coupled, hard to test, and less maintainable.</p>
<p>But when used correctly, the Singleton pattern helps you save memory, enforce consistency, and control object lifecycle beautifully.</p>
<p>The key is understanding your specific use case and choosing the right implementation approach – whether eager, lazy, or factory-based – that best serves your application's needs while maintaining clean, testable code.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ System Architecture Documentation Best Practices and Tools ]]>
                </title>
                <description>
                    <![CDATA[ Imagine being asked to give UX feedback on a system workflow document and realizing you can’t understand a word of it. That’s exactly what happened to me. As an IT support officer, I can put myself in the perspective of a user and identify friction p... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/system-architecture-documentation-best-practices-and-tools/</link>
                <guid isPermaLink="false">691484910576aea108fc08d8</guid>
                
                    <category>
                        <![CDATA[ documentation ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software architecture ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Collaboration ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Ifeoma Udu ]]>
                </dc:creator>
                <pubDate>Wed, 12 Nov 2025 12:58:57 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1762950321590/b67b93ef-de20-430b-a160-13631259c1d5.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Imagine being asked to give UX feedback on a system workflow document and realizing you can’t understand a word of it. That’s exactly what happened to me.</p>
<p>As an IT support officer, I can put myself in the perspective of a user and identify friction points, but this document had no visuals, no simplified explanations, just walls of backend jargon: <em>service mesh, container orchestration, async queues, REST APIs… you name it.</em></p>
<p>I realized quickly: if someone like me struggles to understand this, so will PMs, frontend devs, new hires, and even other IT staff.</p>
<p>Here’s a practical guide for creating system architecture documentation that anyone on your team can read and use:</p>
<ul>
<li><p><a class="post-section-overview" href="#heading-step-1-show-the-system-from-different-angles">Step 1: Show the System from Different Angles</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-make-diagrams-the-star">Step 2: Make Diagrams the Star</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-3-translate-tech-into-user-relevant-outcomes">Step 3: Translate Tech Into User-Relevant Outcomes</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-4-make-communication-clear">Step 4: Make Communication Clear</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-5-keep-it-simple-and-consistent">Step 5: Keep it Simple and Consistent</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-system-architecture-documentation-tools-for-teams">System Architecture Documentation Tools for Teams</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-step-1-show-the-system-from-different-angles"><strong>Step 1: Show the System from Different Angles</strong></h2>
<p>A good architecture doc isn’t just a list of tech terms. Think about who is reading it:</p>
<p><strong>A. Conceptual View (PM/UX/business folks)</strong></p>
<ul>
<li><p>What the system does for the user.</p>
</li>
<li><p>Example: <em>“User Authentication System,” “Checkout Service”</em></p>
</li>
<li><p>Focus on user value and business goals.</p>
</li>
</ul>
<p><strong>B. Component View (frontend developers/IT staff)</strong></p>
<ul>
<li><p>How the parts interact.</p>
</li>
<li><p>Example: <em>“Web App calls API Gateway → Microservice → Database”</em></p>
</li>
<li><p>Focus on data flow and system boundaries.</p>
</li>
</ul>
<p><strong>C.   Operational View (backend/DevOps)</strong></p>
<ul>
<li><p>Where the system runs and how.</p>
</li>
<li><p>Example: <em>servers, databases, cloud setup, scaling.</em></p>
</li>
<li><p>Focus on infrastructure and deployment.</p>
</li>
</ul>
<p>This way, everyone can find what’s relevant to their role without getting lost in technical weeds.</p>
<h2 id="heading-step-2-make-diagrams-the-star"><strong>Step 2: Make Diagrams the Star</strong></h2>
<p>Words alone don’t cut it. Diagrams help people visualize the system, especially if they’re not experts.</p>
<p><strong>Types of Diagrams to Include</strong></p>
<ul>
<li><p><strong>System Context Diagram:</strong> Shows the system and its external dependencies. UX/PM/IT staff can see how it touches users and other systems.</p>
</li>
<li><p><strong>Container Diagram:</strong> Shows main boundaries like <em>“Web App,” “Auth API,” “Database.”</em> Frontend and backend teams benefit.</p>
</li>
<li><p><strong>UML/Component Diagram:</strong> Shows internal structure or interactions. Mostly backend focus, but helps everyone understand flow.</p>
</li>
</ul>
<p><strong>Tip:</strong> Even a simple flowchart drawn in PowerPoint, Figma, or by hand is better than none. Clarity matters more than perfection.</p>
<p>Diagrams help:</p>
<ul>
<li><p>UX sees user impact.</p>
</li>
<li><p>Frontend knows which services to hook up.</p>
</li>
<li><p>Backend sees infrastructure and interactions.</p>
</li>
<li><p>Everyone shares the same mental picture.</p>
</li>
</ul>
<h3 id="heading-example-1-system-context-diagram">Example 1: System Context Diagram</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762421963002/ab9f11d4-e30f-4e4a-9510-55b4b3f5e8ad.jpeg" alt="Flowchart illustrating an end user visiting a web app, which processes payments via Stripe API and sends emails through SendGrid using webhooks." class="image--center mx-auto" width="1498" height="432" loading="lazy"></p>
<p>This diagram illustrates who uses the system (a person on the web) and which external services it depends on, like Stripe for payments and SendGrid for emails. It doesn't show the internal workings of the system, just what it connects to.</p>
<h3 id="heading-example-2-container-diagram">Example 2: Container Diagram</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762612172310/1ef1b7e9-0441-4d34-b136-2283ec0b4c56.jpeg" alt="Flowchart depicting a web application architecture. The sequence starts with a web browser, leading to a frontend app, then to an API gateway. The gateway splits into two paths: one leads to an Auth Service connected to a User Database, and the other to an Order Service connected to an Orders Database." class="image--center mx-auto" width="505" height="636" loading="lazy"></p>
<p>This diagram illustrates the main components of the system: the Web App, which is the user interface; the Auth API, responsible for handling login and security; and the User Database, where user profiles are stored. The arrows indicate how these components interact with each other.</p>
<p><strong>Practical Tip: Tools to Create Clear Architecture Docs.</strong></p>
<h2 id="heading-step-3-translate-tech-into-user-relevant-outcomes"><strong>Step 3: Translate Tech Into User-Relevant Outcomes</strong></h2>
<p>System architecture goes beyond databases and queues, focusing on making the product fast, reliable, and secure for users. Link technical requirements to outcomes everyone can understand:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Requirement</strong></td><td>❌ <strong>Technical Jargon</strong></td><td>✅ <strong>User Outcome</strong></td></tr>
</thead>
<tbody>
<tr>
<td>Scalability</td><td>Kubernetes for container orchestration</td><td>Can handle 10x daily users without slowdowns.</td></tr>
<tr>
<td>Performance</td><td>CDN + caching</td><td>Pages load in under 500ms, no “loading” screens.</td></tr>
<tr>
<td>Security</td><td>TLS 1.3 for data transfer</td><td>User data is safe; only authorized systems access PII(Personally Identifiable Information)<strong>.</strong></td></tr>
</tbody>
</table>
</div><p>Even a person with basic UX awareness can see why tech decisions matter.</p>
<h2 id="heading-step-4-make-communication-clear"><strong>Step 4: Make Communication Clear</strong></h2>
<p>One major source of confusion is how different parts of the system talk to each other. Spell it out:</p>
<p>a) Frontend ↔ Backend: Clearly explain how your frontend connects to the backend.</p>
<p>Example: <em>“The website sends login requests to the Auth API.”</em></p>
<p>b) Backend ↔ Backend: Explain whether services communicate with each other instantly (synchronous) or through background tasks like message queues (asynchronous). This helps the team understand why some actions feel instant to users, while others take time.</p>
<p>Even non-backend readers can follow the flow and understand how it impacts the product.</p>
<h2 id="heading-step-5-keep-it-simple-and-consistent"><strong>Step 5: Keep it Simple and Consistent</strong></h2>
<ul>
<li><p>Use headings, bullet points, and a table of contents. Don’t write a novel.</p>
</li>
<li><p>Keep names consistent: <em>“User Service”</em> in diagrams should match text labels.</p>
</li>
<li><p>Explain the “why” behind major decisions:</p>
</li>
</ul>
<p><em>“We chose a NoSQL DB for User Profiles because it requires fast read/write for non-relational data.”</em></p>
<p>Consistency and simplicity make the doc useful to everyone, not just backend experts.</p>
<h2 id="heading-system-architecture-documentation-tools-for-teams"><strong>System Architecture Documentation Tools for Teams</strong></h2>
<p>Great architecture documentation lives where your team already works and uses tools that are easy to update, share, and understand. Below are the common types of tools teams use, grouped by purpose.</p>
<h3 id="heading-documentation-platforms-where-you-write-the-full-doc">Documentation Platforms (Where You Write the Full Doc)</h3>
<p>You can use these tools to combine text, diagrams, and structure a document.</p>
<p><strong>Google Docs</strong><br>Simple, familiar, and collaborative. Supports real-time comments, edit history, and easy sharing. Perfect if your team already uses Gmail or Drive. Just paste diagrams as images.</p>
<p><strong>Confluence</strong><br>Common in larger companies. Integrates with Jira, supports page templates, and lets you embed diagrams. Good for structured knowledge bases.</p>
<p><strong>Notion</strong><br>Flexible workspace for small teams. Mix docs, tasks, and diagrams in one place. Great if your team uses Notion for other work.</p>
<p><strong>GitHub/GitLab Wikis (with Markdown)</strong><br>Ideal for engineering-heavy teams. Docs live next to your code, and you can include diagrams using simple code (like Mermaid). Changes are tracked like code.</p>
<blockquote>
<p><strong>Start with Google Docs</strong> if you’re unsure. A living doc people actually read is better than a “perfect” one no one opens.</p>
</blockquote>
<h3 id="heading-diagramming-tools-where-you-create-visuals"><strong>Diagramming Tools (Where You Create Visuals)</strong></h3>
<p>These help you draw the architecture diagrams you’ll add to your documentation platform.</p>
<p><strong>Draw.io</strong><br>Free, browser-based, and drag-and-drop simple. No sign-up needed. Exports clean PNG/SVG files you can paste into Docs or Confluence. Great for C4-style diagrams (System Context, Container, and so on).</p>
<p><strong>Figma</strong><br>If your team already uses Figma for design, you can create architecture diagrams using basic shapes and arrows. Real-time commenting makes feedback easy. Just export as PNG for Docs.</p>
<p><strong>Mermaid (Diagrams as Code)</strong><br>Write simple text like <code>User --&gt; Web App</code>, and it becomes a diagram. Works in GitHub, GitLab, and tools like Obsidian. Use the <a target="_blank" href="https://mermaid.live/">Mermaid Live Editor</a> to design, then download and paste into Google Docs.</p>
<h4 id="heading-a-key-insight-from-practicing-architects"><strong>A Key Insight from Practicing Architects.</strong></h4>
<p>Avoid tools that <strong>only produce static images</strong> (like PowerPoint, Canva, or basic whiteboards) for anything beyond quick sketches. If the same service appears in three diagrams and you rename it, you’ll have to update all three manually, leading to outdated and inconsistent docs.</p>
<h3 id="heading-how-they-work-together"><strong>How They Work Together</strong></h3>
<ol>
<li><p>Write your doc in Google Docs (or your team’s existing platform).</p>
</li>
<li><p>Create diagrams in Draw.io or Figma (or try Mermaid if you’re curious).</p>
</li>
<li><p>Paste the diagram into your doc, add alt text, and explain what it shows in plain language.</p>
</li>
</ol>
<p>This combo gives you accessibility, collaboration, and maintainability without overwhelming you or your team.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>You don’t need to be a senior engineer to write great architecture docs. You just need clarity, empathy, and the willingness to explain “why.”</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
