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:
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:
- The business logic moves out of the forms into a library (lesson M3.1).
- A web API makes the library available (lesson M4.1). It is the interface that web and Windows program share.
- 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).
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.
<#
.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
./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:
- Goal: one sentence on what is different after the modernization, from the business's point of view.
- Starting point: the reference to the map from lesson M1.3.
- Seams: the table of seams with benefit and risk.
- Order: the result of the script with the deviations and their reasons.
- Safety net: which tests and copies come before every stage (module M2).
- Fallback plan: per stage, how you return to the old version in minutes.
- 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
# 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
# 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
- Strangler Fig pattern: the pattern with phases, conditions, contraindications and the example with the database.
- Anti-corruption Layer pattern: the adapter between old and new.
- Overview of upgrading .NET apps: the recommendation to order by risk and yield and to start with a pilot.