Migrating a property agency off a twenty-year-old platform
Six thousand properties. One cutover window. No lost trust money.

~6,500 properties and ~6,300 active tenancies moved as one coordinated cutover. 250,000+ contact records reconciled across two target systems. ~7,800 data issues found, resolved to 21 owned items.
Delivered in a senior enterprise role, not a Burl client engagement, and anonymised. It is here because the discipline it took, regulated data proven to the cent, is exactly what we bring to this kind of work.
The problem
A property agency ran on a platform installed twenty years ago. It was built in the UK, with a data model shaped by conventions that do not match Australian property or Australian accounting. Two decades of tenancies, owners, trust ledgers, compliance certificates and maintenance history sat inside it. Everyone depended on it every day, and almost no one knew how it actually stored what it stored.
Moving off it was not a copy job. Every record had to come out clean, land correctly in a completely different data model, and reconcile to the cent, because a large part of it is regulated trust money. A migration that loads without error but puts a lease start a year out, or a rent balance in the wrong place, is not a win. It is a liability that surfaces after go-live, in front of owners and tenants.
The approach
One rule shaped the method: rebuild from source, prove every file. I did not massage the existing exports. Each import file was built from a single query straight against the source replica, so it was always a fresh pull, never a re-derivation of an earlier file. That one rule removed a whole class of silent corruption where files drift out of step with each other.
Underneath it sat three principles. Decisions get locked before any build begins. Data is never excluded quietly, because financial defaults like fee treatment, rent coding and direct-debit rules are business calls, not developer conveniences. And internal consistency is the integrity test: files built from the same pull reconcile against each other, and differences are portfolio movement, not defects.
Reading a system that lied by omission
The real structure was not in the tables. Per-property fees, parking, direct-debit and bond references were crammed into text columns as key-value strings, in two different formats. The relationships you would expect as foreign keys, joint owners, joint tenants, property-to-supplier links, lived inside a single universal junction table nobody had mentioned. Trust the obvious column and you migrate the wrong thing at scale.
So I read the actual data before writing the query. Every proposed root cause was confirmed against real source rows first, never assumed from a table name. When the data disagreed with the intuitive reading, the data won. I refused synthetic data outright: a tempting shortcut that invented joint-tenant records from free-text notes was wrong, so it went in the bin and the correct source was used instead.
Key decisions
I locked the business rules before building. Rent coding, fee treatment, payment splits and direct-debit rules each became a specific question routed to the person accountable for it, and the build waited on the answer rather than shipping a guess. I kept humans on the judgement calls: thirty-four soft-deleted creditors, around one hundred and seventy thousand dollars of balance impact, were excluded only after the business confirmed the call. And I treated trust money as the gate: the first reconciliation after cutover is a statutory three-way balance that must be signed within fourteen days, so I built the migration so that gate could pass, using the platforms’ own native reports rather than a custom tool no auditor would trust.
How I measured “done”
A clean-looking file is not a correct one. A validation run surfaced roughly 7,800 issues. I root-caused them to six underlying causes, fixed the six, and patched the rest down to 21 residual items, each tracked to an owner or a decision. Structural checks ran as hard assertions against the exact file being handed over, not a sample: zero malformed addresses, reconciled record counts, a complete chart of accounts.
One catch mattered more than any single fix. A tester found a raw, ungrossed fee rate in the target system that the current build could not produce. That one contradiction proved the file loaded into the target was not the current build, which meant a share of the “defects” being chased were artefacts of an older load. Catching that before sign-off was the difference between fixing real problems and certifying against the wrong file.
At the gate, the honest call
Banking details for owner disbursements and tenant direct debits did not exist in the source at all. Rather than fabricate or quietly drop them, I named the gap as a blocker. Combined with the open items still in flight, the evidence supported deferring the go-live rather than cutting over into a regulated environment that could not yet pay or collect correctly. The deferral was the right call, and I made it with the numbers to back it.
What I would bring to your project
I scope before I build, and I find the one constraint that should drive the design, here a statutory reconciliation deadline, before a line of code is written. I own the data end to end, source to validation, and talk to the business in its own language. And I make “done” provable: for regulated data, that traceability is the deliverable, not an extra.