Technical Debt Is Quietly Holding Back Business Performance
Date: 18/08/26
By: Digiterre
A design constraint for disaster recovery tooling — resolve every restore against a configuration snapshot captured before the incident, never against a live query — and what falls out of it: pure-function planning, fully offline tests, and drills cheap enough to run continuously
Most disaster recovery runbooks contain a sentence like this one:

It reads fine in a review. It is useless at 02:00, because it quietly assumes you can still find out what the original storage path was — and the system you would normally ask is the one that just broke.
I want to describe the design constraint I ended up adopting to remove that assumption entirely: a restore resolves against a snapshot of the environment captured before the incident, never against a live query. It sounds like a small implementation detail. It is not. Adopting it changes what your recovery code can be tested against, what your drills cost, and — in at least one case I will come to — whether a particular class of recovery is possible at all.
My examples are Azure Databricks with Unity Catalog over ADLS Gen2, but the principle transfers to any platform where the metadata layer and the data layer can fail independently.
Consider a plausible incident: someone drops a schema containing a few hundred Unity Catalog managed tables. The underlying storage has soft delete enabled, so the data is still there. You are, technically, fine.
Then you go and look at the storage account, and find this:

There is nothing here that says which directory was sales.orders and which was sales.order_lines. Unity Catalog knew the mapping. Unity Catalog is the thing that lost it. Every one of those GUID directories holds a perfectly intact Delta table that you cannot identify, and no amount of storage-layer recovery will tell you the names.
The recovery is trivially easy if you wrote the mapping down beforehand, and effectively impossible if you did not. That asymmetry is the whole argument. There is a category of recovery metadata whose only viable capture point is before the incident, and a restore tool that queries live state has, by construction, no access to it.
A snapshot is a configuration capture, not a data backup. It records the shape and intended state of the environment: what exists, where it points, who can read it, and how it is protected. It does not contain a single row of business data.
That distinction matters because it sets expectations correctly in both directions. A snapshot alone cannot restore anything — it always pairs with the file or Delta backup that holds the actual bytes. But the backup alone cannot restore anything usefully either, because it restores a pile of anonymous directories. You need both, and it is worth drawing an explicit coverage matrix showing which recovery scenario needs which half.
A snapshot is also not a monitoring system. It is written on a schedule, retained for a long time, and never overwritten in place. Its value is precisely that it is old.
The rule of thumb: capture anything you would have to look up in order to rebuild, and anything whose absence you would only notice under pressure.
For the Databricks metadata layer:
| Artefact | Why restore needs it |
| Catalogs and schemas, with owners | Recreate the container hierarchy in the right order |
| Catalogs/schema/object grants | Replay permissions; otherwise restored object is invisible to its users |
| Table inventory with physical storage locations | The name → path map, including managed GUID paths |
| External locations and their URLs | Re-register external tables at the correct paths |
| Storage credentials and identity bindings | Reconnect the metadata layer to storage |
| Volumes | Same problem as tables, for non-tabular data |
| Groups and members; users | Grant replay is meaningless without the principals |
| Deployed job/pipeline definitions | Rebuild the workloads that populate the restored tables |
For the Azure layer underneath:
| Artefact | Why restore needs it |
| Storage accounts: resource locks, soft-delete and versioning settings, public network access | Establishes what recovery options actually exist |
| Private endpoints and their connection state | Re-establish the classic connectivity path |
| Serverless network connectivity rules | Re-establish the serverless path, which is separate |
| Key Vault and secret-scope inventory (names, not values) | Know what has to be repopulated, and by whom |
The two connectivity rows deserve a note, because it is a trap I have watched people fall into. A storage account with public network access disabled is typically reachable by two independent private paths: customer-VNet private endpoints for classic compute, and an account-level network connectivity rule for serverless compute. Repairing one does not repair the other, and a successful read over the classic path proves nothing about the serverless path. They are separate rows in the snapshot for exactly that reason — capturing them together is what makes the difference visible before someone assumes it away.
Keep it boring and filesystem-shaped:

Three properties are worth being deliberate about:
YYYY-MM-DD_HHmm gives you “the latest snapshot” as a sorted(...)[-1], with no metadata index to maintain or corrupt.grep them. Do not make that hard.Retention is a real decision, not an afterthought. Ninety days of CI artefacts is a reasonable starting point, but the retention window is also your maximum blast radius for a slow corruption: if a bad change went in four months ago and every snapshot you still hold records the damaged state, the snapshot cannot tell you what “correct” looked like.
Each artefact gets its own small extraction script. They are boring on purpose — query the SDK, normalise, write JSON:
Two conventions that pay for themselves:
Wrap the payload. Write {"generated": "<iso timestamp>", "catalogs": [...]} rather than a bare list. You will want to know when a file was written without trusting the directory name, and you will eventually want to add a field beside the payload.
Record permission failures rather than omitting them. If the caller lacks the rights to read something, write a marker — {"access": "permission_denied"} — instead of an empty list or nothing at all. An empty list is indistinguishable from “there genuinely aren’t any”, and that ambiguity will cost someone an hour during an incident. This happens more than you would expect: some account-level settings are only readable by an account administrator, so a snapshot taken with workspace-level credentials legitimately cannot see them.
The restore tool never touches the snapshot files directly. It goes through one small reader class, which is where all the format tolerance lives:
That _unwrap tolerance is not fussiness. The snapshot format will change, and the snapshots you most need are the old ones. A reader that only understands this month’s format has quietly expired your entire archive.
This is the part that surprised me. I adopted snapshot sourcing for correctness, but most of the benefit turned up somewhere else entirely.
If a restore scenario reads only files, then “work out what we would do” is a pure function from snapshot to plan. So express it that way: every scenario exposes a plan_* function that takes a context and returns an ordered list of steps, and nothing executes while resolving.
Because resolution is pure, the entire test suite runs with no cloud credentials. A pytest fixture builds a miniature snapshot under tmp_path and the tests assert on plans:

And crucially you can test the things that actually go wrong in a restore — that a schema is never created before its catalog, that a grant is never replayed before the object it applies to, that validation happens before consumers are re-pointed and not after. In a restore, the order of operations is the correctness. Ordering is almost impossible to test against a live environment and trivial to test against a fixture.
When a plan needs a field the snapshot does not contain, there is exactly one right answer: fix the extraction script, take a new snapshot, and carry on. The wrong answer — inferring it, defaulting it, reconstructing it from a naming convention — is now visibly wrong, because the code has no live client to infer it from.
Make the degradation explicit and unpleasant. Never guess a storage path, particularly a managed table’s GUID path: a plausible-looking wrong path in a recovery plan is worse than no plan, because it will be run. Emit a step with no action and a manual_reason instead.
That leads to the guardrail I would keep even if I kept nothing else: a plan containing any manual step refuses to execute.

Dry run is the default; --execute is opt-in and refused while anything is unresolved. A step only loses its manual flag once a drill has been recorded for it — automation is earned, scenario by scenario, rather than assumed at design time.
A dry run exercises the whole resolution path — snapshot lookup, path mapping, dependency ordering, SQL generation — while touching nothing. You can run every scenario against last night’s snapshot on a laptop, in seconds, and see the exact commands each one would issue. That is a rehearsal cheap enough to run continuously, which means the runbook stops rotting between annual DR tests.
Two immutable, timestamped captures of the same environment are a diff waiting to happen:

A newly-failing configuration assertion becomes a concrete line of output — this grant was removed, this endpoint went from Approved to Pending — instead of a debate. This is also the natural home for compliance evidence: you are not asserting that permissions were correct on a given date, you are holding the capture.
The obvious default — restore from the most recent snapshot — is wrong, and worth guarding against explicitly.
A scheduled capture keeps running throughout an incident. In a corruption or accidental-deletion event, the newest snapshot may faithfully record the damaged state: the grant already removed, the table already dropped, the endpoint already broken. Restoring from it reproduces the incident with great fidelity.
So make “latest” a convenience and “explicit” a first-class option:

And say so loudly in the runbook, at the top, where someone under pressure will actually read it. The first question of any restore is not “what do I run” but “which snapshot am I trusting, and do I know it predates the incident?”
Three honest limits.
Some checks genuinely cannot be static. Whether a TCP endpoint is currently accepting connections is not a property you can capture in JSON. Keep those as live checks, mark them clearly as such in the code, and accept that they are a different kind of test.
A snapshot is not a data backup, and the failure mode of forgetting that is severe. Draw the coverage matrix, keep it current, and make sure every scenario in the runbook names both halves of what it needs.
Capture is bounded by the credentials it runs with. Some settings need account-level rights that a workspace-scoped identity does not have. Decide deliberately whether to elevate the snapshot job or to accept a documented blind spot — but write the blind spot down, in the snapshot, as a marker.
If you want to try this on an existing platform, the smallest useful increment is not the whole framework. It is one CSV.
plan_* function that reads that inventory and returns steps it refuses to execute. Print it. Show it to whoever owns the runbook.Step 1 alone converts one impossible recovery into a routine one. Everything after it is compounding interest.
The pattern described here came out of building recovery tooling for a Unity Catalog data platform on Azure. The specifics — artefact names, scenario numbering — are implementation detail; the constraint is the transferable part. During an incident, the live environment is not a source of truth about intended state. Write it down first.
Date: 18/08/26
By: Digiterre
Date: 11/08/26
By: Digiterre
Date: 03/08/26
By: Digiterre
Date: 28/07/26
By: Digiterre
If you would like to find out more, or want to discuss your current challenges with one of the team, please get in touch.