Short answer: A typical legacy Delphi system changes slowly because its business rules live inside form event handlers, where no automated test can reach them (test automation for UI is possible, but complex and fragile). Every change needs a manual regression round, so changes are batched into a few large releases, each installed on every desktop. In DORA’s 2024 data, the teams that release least often also have the most failed changes and the slowest recovery. You can measure your own system in about a week with the four DORA metrics, from version control, the ticket system and the incident log.
Key points
- The cause is practice, not the language. Delphi has unit-test frameworks (DUnit, DUnitX) and builds from the command line. Legacy code structure is what makes tests and automation hard to add.
- DORA 2024, elite versus low: on-demand versus monthly to six-monthly releases, under a day versus one to six months of lead time, 5% versus 40% failed changes, under an hour versus a week to a month to recover.
- You can measure your own system in a week from version control, the
incident log and
git loghotspots. - Start with tests that capture today’s behaviour, then extract rules from forms, then automate the build and deployment.
- A web application removes desktop rollout altogether, and with it the days it takes to get a fix onto every PC.
Why does a legacy Delphi system change slowly?
Because the rules that matter are written into the screens. This is the pattern we find in most legacy Delphi code (an illustrative example, not client code):
procedure TOrderForm.btnSaveClick(Sender: TObject);
var
Qty: Integer;
Total: Currency;
begin
Qty := StrToIntDef(edtQty.Text, 0);
if Qty <= 0 then
begin
ShowMessage('Enter a quantity');
Exit;
end;
Total := Qty * qryProduct.FieldByName('UNIT_PRICE').AsCurrency;
if (cbTier.Text = 'A') and (Total > 1000) then
Total := Total - Total * 0.05; // tier A volume discount
qryExec.SQL.Text :=
'INSERT INTO ORDERS (CUST_ID, QTY, TOTAL) VALUES (' +
IntToStr(FCustId) + ', ' + IntToStr(Qty) + ', ' +
CurrToStr(Total) + ')';
qryExec.ExecSQL;
ShowMessage('Order saved');
end;
Validation, pricing and persistence sit in one click handler. The discount
rule can only be checked by opening the form, picking a tier and pressing
the button. There is a latent defect too: CurrToStr uses the PC’s decimal
separator, so with Hungarian regional settings the total gets a comma and
the SQL breaks.
The same rule, extracted into a plain unit, can be tested in milliseconds:
unit Pricing;
interface
function OrderTotal(Qty: Integer; UnitPrice: Currency;
const Tier: string): Currency;
implementation
uses System.SysUtils;
function OrderTotal(Qty: Integer; UnitPrice: Currency;
const Tier: string): Currency;
begin
if Qty <= 0 then
raise EArgumentException.Create('Quantity must be positive');
Result := Qty * UnitPrice;
if (Tier = 'A') and (Result > 1000) then
Result := Result - Result * 0.05;
end;
end.
unit PricingTests;
interface
uses DUnitX.TestFramework;
type
[TestFixture]
TPricingTests = class
public
[Test] procedure TierAOver1000GetsFivePercentOff;
[Test] procedure TierAAtExactly1000GetsNoDiscount;
[Test] procedure ZeroQuantityIsRejected;
end;
implementation
uses System.SysUtils, Pricing;
procedure TPricingTests.TierAOver1000GetsFivePercentOff;
begin
Assert.AreEqual<Currency>(1140, OrderTotal(12, 100, 'A'));
end;
procedure TPricingTests.TierAAtExactly1000GetsNoDiscount;
begin
Assert.AreEqual<Currency>(1000, OrderTotal(10, 100, 'A'));
end;
procedure TPricingTests.ZeroQuantityIsRejected;
begin
Assert.WillRaise(
procedure begin OrderTotal(0, 100, 'A') end, EArgumentException);
end;
initialization
TDUnitX.RegisterTestFixture(TPricingTests);
end.
The form now calls OrderTotal and saves the result with a parameterised
query. DUnitX needs Delphi XE2 or later; a Delphi 7 system can use the older
DUnit framework in the same way.
Multiply the first pattern across a few hundred forms, and a chain follows:
- No automated tests, so every change needs a manual regression round.
- Manual regression is expensive, so changes wait and are batched.
- Big batches are risky: many changes ship at once, and a failure is hard to trace to one of them.
- Builds and installs are manual. The executable is built on one PC and copied to every desktop or a Citrix server. A fix takes days to reach everyone.
- People become afraid to touch the code, and the backlog grows.
How do you measure the speed of change?
Use the four DORA metrics: deployment frequency, lead time for changes, change failure rate and failed deployment recovery time. DORA (DevOps Research and Assessment, Google’s research programme, unrelated to the EU regulation) has used them for over a decade. Lead time runs from commit to production.
In the 2024 Accelerate State of DevOps report, four clusters emerged:
| Cluster (share of respondents) | Deployment frequency | Lead time for changes | Change failure rate | Failed deployment recovery time |
|---|---|---|---|---|
| Elite (19%) | On demand, multiple per day | Under one day | 5% | Under one hour |
| High (22%) | Daily to weekly | One day to one week | 20% | Under one day |
| Medium (35%) | Weekly to monthly | One week to one month | 10% | Under one day |
| Low (25%) | Monthly to every six months | One to six months | 40% | One week to one month |
These are typical values for each cluster, not thresholds, and DORA warns against using them as a maturity model. The 2025 report no longer groups teams into these four levels. It describes seven team archetypes instead, which also take in well-being and product outcomes. One of them is “the legacy bottleneck”, teams “trapped by technical debt”: 11% of respondents. The metrics themselves remain, and since 2024 DORA also tracks a fifth, rework rate.
The time cost is real too. In Stripe’s 2018 Developer Coefficient survey, developers estimated that their colleagues spend 17.3 hours of a 41.1-hour week on maintenance: bad code, errors, debugging and refactoring. That is a self-reported estimate across all software, not legacy systems alone.
How to measure your own system in a week
Everything you need is already in version control, the ticket system and the incident log.
-
Deployment frequency. Count production releases, including hotfixes, over the last 12 months, from release tags or the installer archive.
-
Lead time for changes. For each release, take the commits it contains (
git log --format="%cs %h" v1.4..v1.5) and subtract each commit date from the release date. Report the median. Also compute ticket-created to released: it is not a DORA metric, but it is the delay the business feels. -
Change failure rate. Divide the releases that needed a hotfix, rollback or emergency patch by all releases.
-
Recovery time. From the incident log: time from the incident being raised to the fix running on the last affected desktop, not just built.
-
Hotspots. Files that change often and are large are where change is slow and risky. Change frequency per file:
git log --since="12 months ago" --name-only --format="" -- "*.pas" \ | sort | uniq -c | sort -rn | head -20Add each file’s line count. On Subversion,
svn log -vgives the same data. No version control at all is your first finding. -
Test coverage of business rules. List the 20 to 30 rules the business cares about most (pricing, tax, credit limits, stock) and count how many have an automated test. Line coverage, for example from the open-source DelphiCodeCoverage tool, is a useful second view.
A worked example, with illustrative numbers (not a client system):
| Measure (illustrative) | Value | Closest 2024 DORA cluster |
|---|---|---|
| Releases in 12 months | 6 (4 planned, 2 hotfixes) | Low |
| Median lead time, commit to production | 47 days | Low |
| Median ticket to production | 96 days | (not a DORA metric) |
| Change failure rate | 2 of 6 = 33% | Low |
| Median recovery time | 9 days | Low |
| Business rules with an automated test | 0 of 25 |
| Hotspot (illustrative) | Commits, 12 months | Lines | Tests |
|---|---|---|---|
OrderForm.pas |
58 | 6,900 | None |
InvoiceForm.pas |
41 | 4,200 | None |
MainDataModule.pas |
37 | 3,100 | None |
In this example, three large, untested files take most of the changes. That is where to start.
What speeds change up?
Tests first, then structure, then automation. In that order, because each step makes the next one safe.
- Characterisation (golden-master) tests. Record what the system does today, right or wrong, for a large set of real inputs, and compare every future build against it. Michael Feathers describes the technique in Working Effectively with Legacy Code.
- Extract the rules from the forms into units like
Pricingabove, hotspots first, with unit tests around each. - A CI build. Every commit is built on a server and runs the tests.
- Automated deployment. For a desktop system, an automatic updater. For a web application, one deployment to the server, and no desktop rollout at all.
Moving to a web back end and front end (usually Java with Spring Boot and React; C# or another stack where it fits better), with the tests and the pipeline built in from the first slice, changes each metric:
- Deployment frequency rises, because a release is one pipeline run, not an installation round.
- Lead time falls, because a change no longer waits for the quarterly batch.
- Change failure rate falls, because releases are small and every one runs the full test suite.
- Recovery time falls to the time it takes to redeploy the previous version.
When not to bother
If the system changes a few times a year and its releases rarely fail, your numbers will show it, and the money is better spent elsewhere. A Java system without tests or a pipeline changes just as slowly as the Delphi one; DORA’s metrics apply regardless of technology. The cheaper alternative is characterisation tests, a CI build and an automatic updater for the Delphi system itself. That is also the first step of a migration, so the work is not wasted.
How we approach it
The order above is our migration method: capture behaviour with characterisation tests, move the rules out of the forms behind an API, then move slice by slice with a parallel run. See How we migrate Delphi to Java and the web, slice by slice. A baseline measured first shows whether the new system really changes faster.
Frequently asked questions
Do we need special tools to measure the DORA metrics?
No. Git or Subversion, the ticket system and a spreadsheet are enough for a first baseline.
Can we add tests to Delphi code without rewriting it?
Yes. Characterisation tests drive the system from outside, through its database or its outputs, so they need no change to the code. Unit tests come as rules are extracted.
Will migrating to Java make us faster on its own?
No. The speed comes from tests, small releases and automated deployment. Migration makes those practices easier to adopt and keep, and removes the desktop rollout.
Want your own numbers? Talk to an engineer about a technical audit, or see how our re-engineering practice works. For the wider business case, see Moving off Delphi: the business case in numbers.
Sources
Checked 1 October 2026.
- DORA, Accelerate State of DevOps Report 2024 (v. 2024.3): performance clusters, pp. 13–14; industry, p. 15
- DORA / Google Cloud, 2025 State of AI-assisted Software Development (v. 2025.2): metric definitions, p. 13; team archetypes, pp. 15–17; announcement, 23 Sep 2025
- Stripe and Harris Poll, The Developer Coefficient (2018)
- VSoft Technologies, DUnitX (README and
DUnitX.Assert.pas) - DelphiCodeCoverage
- Michael C. Feathers, Working Effectively with Legacy Code, Prentice Hall, 2004
- Adam Tornhill, Your Code as a Crime Scene, 2nd edition, Pragmatic Bookshelf, 2024 (hotspot analysis)