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 log hotspots.
  • 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:

  1. No automated tests, so every change needs a manual regression round.
  2. Manual regression is expensive, so changes wait and are batched.
  3. Big batches are risky: many changes ship at once, and a failure is hard to trace to one of them.
  4. 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.
  5. 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.

  1. Deployment frequency. Count production releases, including hotfixes, over the last 12 months, from release tags or the installer archive.

  2. 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.

  3. Change failure rate. Divide the releases that needed a hotfix, rollback or emergency patch by all releases.

  4. Recovery time. From the incident log: time from the incident being raised to the fix running on the last affected desktop, not just built.

  5. 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 -20

    Add each file’s line count. On Subversion, svn log -v gives the same data. No version control at all is your first finding.

  6. 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.

  1. 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.
  2. Extract the rules from the forms into units like Pricing above, hotspots first, with unit tests around each.
  3. A CI build. Every commit is built on a server and runs the tests.
  4. 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.