After the inventory, Ms. Aydin asks what every domain expert asks: “So where do we start? Can't we just bring the customers section to the web first?” Maybe we can. But maybe the customers section depends on things that have to be finished before it. This lesson gives you a pattern that puts the question in order, and a small script that justifies the sequence.

The pattern is called strangler fig, after a plant that gradually grows around a tree until the tree is no longer needed. It is the pattern of this course: the legacy program keeps running, the new one grows next to it, and in the end the old one is switched off. K4, module 4.11 (architecture styles) shows it in breadth; here you apply it to a desktop program.

The pattern according to Microsoft Learn

Microsoft's description starts from the problem: the older a system gets, the more likely its development tools, its hosting technology and its architecture are outdated, and with every new feature it becomes harder to maintain. Replacing a complex system all at once is difficult. Replacing it gradually has a problem of its own: whoever runs two versions in parallel has to tell the clients which function lives where.

The solution consists of a facade (a proxy) that accepts requests and passes them on either to the legacy system or to the new system. The customers keep using the same interface and don't notice that a migration is going on. The pattern runs in four phases:

mermaid
flowchart LR
    subgraph P1["Phase 1: introduce the facade"]
        C1[Client] --> F1[Facade]
        F1 -->|almost everything| A1[Legacy system]
        F1 -->|a little| N1[New system]
    end
    subgraph P2["Phase 2: move gradually"]
        C2[Client] --> F2[Facade]
        F2 -->|less| A2[Legacy system]
        F2 -->|more| N2[New system]
    end
    subgraph P3["Phase 3: switch off the legacy system"]
        C3[Client] --> F3[Facade]
        F3 --> N3[New system]
    end
    subgraph P4["Phase 4: remove the facade"]
        C4[Client] --> N4[New system]
    end
    P1 --> P2 --> P3 --> P4

Microsoft also names points you have to keep in mind:

  • Shared data. Services and data stores that old and new both use have to be reachable at the same time.
  • Clear boundaries. Build the new parts so that they can easily be intercepted and replaced again later.
  • The facade is temporary. Normally you remove it at the end. It can also stay as an adapter for old clients. Weigh the benefit against the cost of this transitional architecture.
  • The facade must not become a bottleneck, neither as a point of failure nor in performance, and it has to keep pace with the migration.
  • Dependencies between the systems. Old and new have to call each other for a while. For that, Microsoft recommends the anti-corruption layer: an adapter that translates calls and protects the design of the new system from the concepts of the old one.
  • Legacy parts that can't be changed. In database-centric applications, the database itself can send messages to new services without the legacy program changing. Microsoft names a trigger or change data capture for this.

The example in Microsoft's documentation applies the pattern to a database: a new service first serves its tables in the old database, then it gets a database of its own that is kept in sync by copying and change tracking. After a check, it becomes the leading system for that area, and only then are the old tables removed. Until then, the way back is open; afterwards it costs a lot of effort and risk. Removing old objects is therefore a deliberate last step per area.

When the pattern doesn't fit

Microsoft names four cases in which it may be unsuitable:

Case Why
Requests to the legacy system can't be intercepted Without a place to redirect, there is no facade
no access to the source code and no other changeable interface (such as the database) Redirected functions have to be switchable off in the legacy system
a small system that is simple to replace The effort of the pattern exceeds the benefit
the original has to be replaced completely and quickly The pattern presupposes a long period of parallel running

Do all four apply to the Order Book? The third and fourth cases don't: the program is small, but its rules are not, and it doesn't have to be replaced quickly. Let's check the first two.

The pattern for a desktop program

The Order Book has a problem with the first case: there are no requests a proxy could intercept. The program runs on Ms. Aydin's computer and talks to the database directly with ADO.NET. There is no place in the network where you could redirect. The second case doesn't apply: you have the source code, can change the legacy program, and the database is a second interface you can change.

That is why, for a desktop program, the facade is not a proxy but a shared layer that old and new both access. It comes about in three steps, which this course builds one after the other:

  1. The business logic moves out of the forms into a library (lesson M3.1).
  2. A web API makes the library available (lesson M4.1). It is the interface that web and Windows program share.
  3. The Windows program calls the API, function by function, with a switch per function (lesson M4.2). That is the replacement for the proxy's redirection: the switch decides whether a function runs old (database) or new (API).
mermaid
flowchart TD
    subgraph Today
        F0[Forms] --> D0[DataSet] --> DB0[(Database)]
    end
    subgraph After_M3[After module 3]
        F1[Forms] --> L1[Library] --> DB1[(Database)]
    end
    subgraph After_M4[After module 4]
        F2["Forms, function by function"] -->|Switch| API[Web API]
        W2[Blazor pages] --> API
        API --> L2[Library] --> DB2[(Database)]
    end
    Today --> After_M3 --> After_M4

This is an adaptation of the pattern, not Microsoft's wording. The phases stay the same: introduce a shared place, move function by function, switch off the old, tear down the intermediate layer. The requirements from Microsoft's list apply unchanged: the shared database needs clear owners (lesson M3.3), old and new need an anti-corruption layer where they call each other, and the facade is a transitional architecture (lesson M6.5 tears it down again).

Finding seams

A seam is a place where the program can be split with little effort, because little passes through there. Good seams have five properties:

Property Question
narrow interface How many arrows of the map cross the cut?
its own data Does a piece of data clearly belong on one side of the cut?
testable beforehand Can the behavior be pinned down before anything is changed (lesson M2.1)?
nameable in business terms Does Ms. Aydin know the name (“pricing rules”, “maintenance plan”)?
deliverable on its own Is the cut useful by itself, even if the rest has to wait?

Here is how you find them in the map from lesson M1.3: draw lines through the diagram in your mind and count the arrows they cross. Few arrows, one seam. The break points from lesson M1.2 are candidates, because each is a place where something has to be pulled out. For the Order Book, that gives nine:

Id Seam Break point What the line crosses
N1 Pricing rules from MainForm into a library B5 two callers (printing, export), inputs from a row and the configuration
N2 Configuration from App.config B2 Settings is the only class that knows App.config
N3 Data access: replace the DataSet with Entity Framework Core B4 every form, hence wide
N4 Maintenance rule: trigger and procedure into the library B6 invisible: no program calls the trigger
N5 Order list as a web API and Blazor page B4, B11 data, pricing rules, state
N6 Invoice printing as PDF B7 InvoicePrint and pricing rules
N7 File export to Blob Storage B8 the folder on the network drive, a reader on the other side
N8 Sign-in with Microsoft Entra ID B3 permissions in every function
N9 Customer card with WebView2 B9 one form, no data access

Two seams are deceptive. N3 looks like a seam but is a wall: every form hangs on it. And N4 has not a single caller that you could find. That is exactly why it is risky: whoever overlooks it loses the rule.

Order by risk and benefit

Each seam gets two numbers from one to five: benefit (what the business gets from it, the people too, not only the technology) and risk (how likely is a mistake, how big would it be). They are judgments, not measurements. Write down who assigned them and why; what the team doesn't know stays an open question. That yields a value, benefit divided by risk: a lot of benefit at little risk comes first. Microsoft's documentation on the pattern describes the same goal: replacements with a high yield should come before those with a low one.

One rule is added that overrides the value: No seam comes before its prerequisite. The order list on the web (N5) has a high benefit, but it needs the pricing rules (N1) and the new data access (N3). The prerequisites are in the file:

Id;Seam;Benefit;Risk;Prerequisite
N1;Pricing rules from MainForm into a library;5;2;
N2;Configuration from App.config;3;1;
N3;Data access: replace the DataSet with Entity Framework Core;4;4;N1
N4;Maintenance rule: trigger and procedure into the library;4;5;N1,N3
N5;Order list as a web API and Blazor page;5;3;N1,N3
N6;Invoice printing as PDF;3;3;N1
N7;File export to Blob Storage;2;2;N1
N8;Sign-in with Microsoft Entra ID;4;4;N5
N9;Customer card with WebView2;2;1;

A script orders the seams: from the seams whose prerequisites are done, it picks the one with the highest value each time. It only reads, it checks that every prerequisite is a seam that exists, and it aborts if prerequisites run in a circle.

PowerShell
<#
.SYNOPSIS
Orders the seams from docs/seams.csv by benefit per risk and makes sure no seam comes before its prerequisite.
Only reads, changes nothing.

.EXAMPLE
./scripts/Rank-Seams.ps1 -File ./docs/seams.csv
#>
[CmdletBinding()]
param(
    [Parameter(Mandatory)]
    [string] $File
)

$ErrorActionPreference = 'Stop'
$seams = @(Import-Csv -LiteralPath $File -Delimiter ';' -Encoding utf8)
$ids = $seams.Id

foreach ($seam in $seams) {
    foreach ($id in ($seam.Prerequisite -split ',' | Where-Object { $_ })) {
        if ($id -notin $ids) {
            throw "$($seam.Id) requires $id, but there is no seam $id."
        }
    }
}

$done = [System.Collections.Generic.List[string]]::new()
$remaining = [System.Collections.Generic.List[object]]::new($seams)
$rank = 0
$result = while ($remaining.Count -gt 0) {
    $ready = @($remaining | Where-Object { @($_.Prerequisite -split ',' | Where-Object { $_ -and $_ -notin $done }).Count -eq 0 })
    if ($ready.Count -eq 0) {
        throw "The prerequisites form a cycle: $(($remaining.Id) -join ', ')."
    }

    $next = $ready | Sort-Object { [double]$_.Benefit / [double]$_.Risk } -Descending | Select-Object -First 1
    $remaining.Remove($next) | Out-Null
    $done.Add($next.Id)
    $rank++
    [pscustomobject]@{
        Rank = $rank
        Id = $next.Id
        Seam = $next.Seam
        Value = [math]::Round([double]$next.Benefit / [double]$next.Risk, 2)
    }
}

$result | Format-Table -AutoSize | Out-String -Width 200
PowerShell
./scripts/Rank-Seams.ps1 -File ./docs/seams.csv

Rank Id Seam                                                        Value
---- -- ----                                                        -----
   1 N2 Configuration from App.config                                3.00
   2 N1 Pricing rules from MainForm into a library                   2.50
   3 N9 Customer card with WebView2                                  2.00
   4 N3 Data access: replace the DataSet with Entity Framework Core  1.00
   5 N5 Order list as a web API and Blazor page                      1.67
   6 N6 Invoice printing as PDF                                      1.00
   7 N7 File export to Blob Storage                                  1.00
   8 N8 Sign-in with Microsoft Entra ID                              1.00
   9 N4 Maintenance rule: trigger and procedure into the library     0.80

Don't read the order as a command. N3 comes before N5 even though N5 has the higher value, because N5 requires N3. N4 comes last because its risk is the greatest and its benefit only arises with the new data access. And the configuration (N2) comes first, because it costs little and holds nothing else up. The team may change the order, but every change needs a sentence saying why. An example: sign-in (N8) can come earlier if the first pilot group needs it.

The plan as a document

The result of this module is a document in the repository, docs/modernization-plan.md. It has seven sections:

  1. Goal: one sentence on what is different after the modernization, from the business's point of view.
  2. Starting point: the reference to the map from lesson M1.3.
  3. Seams: the table of seams with benefit and risk.
  4. Order: the result of the script with the deviations and their reasons.
  5. Safety net: which tests and copies come before every stage (module M2).
  6. Fallback plan: per stage, how you return to the old version in minutes.
  7. Decisions: the references to the ADRs, such as the one from lesson M1.1.

The plan is a working document: it changes when the situation changes, and every change is a commit with a sentence saying why. A decision that is hard to reverse additionally gets an ADR; running old and new side by side is one (see the solution to Exercise 2).

What can go wrong

  • The facade stays forever. Microsoft calls it a transitional architecture with costs of its own. Plan its teardown at the start (lesson M6.5).
  • The facade becomes a bottleneck. An API that passes everything through is a point of failure. Observe it from the start (lesson M6.4).
  • A shared database without an owner. When old and new write to the same table without it being settled who writes when, conflicts arise (lessons M3.3 and M6.2).
  • Seams that aren't. “Everything to the web” is not a seam. Break it down until every seam has a name Ms. Aydin understands.
  • Order by fun. The most appealing piece first is tempting. The script forces you to write down the benefit to the business and the risk.
  • Removing old objects too early. Microsoft warns: whoever removes tables and procedures from the old database before the new one has been verified loses the way back.
  • The plan goes stale. A plan nobody keeps current is wrong after a month and ignored after that.

Exercise 1

Create docs/seams.csv and scripts/Rank-Seams.ps1 in your repository and run the script. Then change two numbers the way your team thinks is right, and watch whether the order changes. After that, test the script's safeguards: give N4 a prerequisite N10 that doesn't exist, and then make two seams require each other (N8 requires N9 and N9 requires N8).

How to check your result: In the normal case the script ends with exit code 0; with the two errors it ends with a clear message and exit code 1.

Solution 1

A prerequisite that doesn't exist is reported by the script with the name of the seam and the missing Id:

N4 requires N10, but there is no seam N10.

A loop of prerequisites is reported like this:

The prerequisites form a cycle: N8, N9.

Both messages come from a throw; PowerShell shows them with the location in the script, and pwsh -File ends with exit code 1. If you change a number, say the benefit of the customer card (N9) from 2 to 5, the seam moves forward, because its value (5 divided by 1) is greater than that of the pricing rules. If you think that's wrong, the number is wrong, not the script. Discuss the number, not the order.

Exercise 2

Write docs/modernization-plan.md with the seven sections. For the Order Book, use the results of this lesson and the previous one. For each of the first three seams, the plan names the test that has to be in place beforehand and the fallback. Also create an ADR: docs/adr/0002-old-and-new-side-by-side.md, the decision to keep the legacy program until the end.

How to check your result: Every seam in the plan has a fallback that works without a new delivery of the legacy program, and the ADR names at least two options.

Solution 2

markdown
# Modernization plan: Order Book

## Goal

Ms. Aydin and Mr. Kowalski can also view and edit orders in the browser and from the phone. The legacy program keeps running
unchanged until it is switched off and is the fallback at any time.

## Starting point

See docs/map.md: a Windows Forms project on .NET Framework 4.8, DataSet bound directly, pricing rules in the main form,
maintenance rule in trigger and procedure.

## Seams

| Id | Seam | Benefit | Risk | Source of the numbers |
| --- | --- | --- | --- | --- |
| N1 | Pricing rules into a library | 5 | 2 | team, after lesson M1.3 |
| N2 | Configuration from App.config | 3 | 1 | team |
| N3 | Data access with Entity Framework Core | 4 | 4 | team |

(further seams as in docs/seams.csv)

## Order

Result of scripts/Rank-Seams.ps1: N2, N1, N9, N3, N5, N6, N7, N8, N4. Deviation: N8 (sign-in) moves ahead of the pilot group, because
Mr. Kowalski should only get at the plan with sign-in.

## Safety net

Before N1: tests that pin down the four pricing rules for one sample order each. Before N3: a copy of the database and tests that compare the
results of the queries old and new. Before every seam: a commit that builds.

## Fallback plan

N1: The main form uses its own methods again (one commit back). N2: App.config stays alongside appsettings.json
until the seam is complete. N3: The legacy program keeps reading through the DataSet; the copy of the database is ready.

## Decisions

- 0001: Modernize the Order Book
- 0002: Run old and new side by side
markdown
# 0002: Run old and new side by side

- Status: accepted
- Confidence: medium

## Context

The Order Book is replaced step by step (ADR 0001). Ms. Aydin must not be without a program on any day. The legacy program can be
changed, the source code is available.

## Options

### Option 1: Old and new side by side, switch function by function

One shared API and library, the legacy program calls it per function through a switch. Fallback in minutes, but two
worlds over a longer time.

### Option 2: Cutover on a fixed day

The new one is built completely and introduced on one day. No dual running, but the fixed day carries all of the risk.

### Option 3: Proxy in front of the legacy program

A proxy redirects requests. Doesn't fit here, because the desktop program doesn't send requests that can be intercepted.

## Decision

We take option 1, because it allows the fallback in minutes and divides the risk into small stages. Option 3 is ruled out, because there are
no requests to redirect.

## Trade-offs

For a while we maintain two programs and have to settle who writes which data. The goal “simple operation” pays for this.

## Consequences

A web API and a switch per function come into being; the facade is torn down again at the end.

## Security and privacy consequences

The API is a new attack surface and needs sign-in and permissions before a function is switched over.

## Revisit when

dual running lasts longer than two stages without a switch-off, or the data in old and new drift apart.

What you can do now

  • You explain the strangler fig pattern with its four phases, its conditions and the cases where it doesn't fit.
  • For a desktop program, you use a shared API and library as the facade where a proxy can't intercept anything.
  • You find seams in the map and judge them by interface, data and testability.
  • You order the seams by benefit and risk without putting a seam before its prerequisite.
  • You record the modernization plan as a document in the repository.

With the next file, you test this ability on a second program: in the module exam on map and order.

Further reading