Engineering

How Do You Test Restores Before a Custom Software Development Pilot?

Written by:Better SoftwareBetter Software TeamUpdated 14 min read
A restored database is cut open to reveal a recovery cutoff and a record that fails to fit its expected slot.

Quick Answer

Before accepting a fintech pilot, restore an agreed recovery point into an isolated environment. Compare recovered transaction identities and fields with an independently reviewed manifest, test that the comparison rejects plausible errors, and exercise the application without sending live payments. Keep later external events separate, record recovery time and unresolved gaps, and make that evidence part of custom software development acceptance. A successful restore job alone is insufficient.

We use restore testing to check that recovering saved data produces a usable system, with evidence for the work it must support.

For a fintech practitioner preparing a pilot, a database that starts is only part of the decision. We also need the right transactions and account relationships.

We would agree on that evidence before inviting pilot users. This guide covers the recovery boundary, record comparison, safe application checks, and acceptance record.

Prove the Restored Records Match the Agreed Pilot Boundary

We would begin restore testing with an independently reviewed expected-data manifest, then compare identities and meaningful fields against the restored export. Counts and totals alone cannot establish record equality.

Our contribution is an offline comparison control with deliberate failure cases. It helps make an acceptance condition executable without pretending to restore a database itself.

1. Freeze the Expected Recovery Boundary

We first name the pilot workflow, the database being recovered, the backup source, and the exact recovery cutoff with its time zone. We also name the person who approves that boundary.

For point-in-time recovery, the chosen instant matters. PostgreSQL documents how a base backup and archived write-ahead log can reconstruct database state at a chosen point.

We would have the database engineer record the actual recovery target and confirm its inclusion semantics. An application timestamp is not automatically a database commit position.

The expected manifest therefore needs an agreed relationship to the recovery target. We would not infer that relationship from a field called created_at.

For the narrow control below, we require committed_at to represent the reviewed inclusion rule. A record at the cutoff belongs in scope; a later record does not.

That convention is part of this example's contract. We would change and retest the control if the real database recovery boundary follows a different rule.

We would build the expected manifest from a separately retained, reviewed source or known test dataset. Generating both files from the restored database would make agreement uninformative.

Our manifest would record its source, extraction procedure, schema version, cutoff, reviewer, and file digest alongside the records. We would preserve the original export before any normalization.

We would also state what the manifest excludes. A sample of transactions provides evidence about that sample; it cannot silently become a claim about the entire database.

Pro tip: We would keep the approved manifest outside the restore destination. That makes an accidental comparison of the restored export with itself easier to detect.

2. Compare Identities and Meaningful Fields

We would compare stable event identities first, then account, currency, amount, state, and the agreed commit timestamp. Each field needs an explicit meaning in the pilot's data model.

The reference control uses integer minor units for amounts. We would document the currency convention and avoid silently converting a decimal amount with floating-point arithmetic.

We would preserve reversal and correction records according to the actual ledger model. Matching a net amount does not establish that the required history survived.

Our control rejects duplicate event identities before building a lookup. Otherwise, converting rows into a dictionary could hide a duplicate by overwriting an earlier row.

We would keep database integrity checks separate. File or page consistency, foreign-key checks, and application-specific accounting rules answer questions that an export comparison cannot.

For this example, we expect exactly six fields. A real application may need a composite key, tenant identifier, sequence number, or additional state; we would adapt the contract explicitly.

The function below performs only offline comparison. We would run it on authorized test exports, never treat its result as permission to reconnect a restored system.

const FIELDS = [
  "event_id", "account", "currency", "amount_minor",
  "state", "committed_at"
]

function timestamp(value) {
  const hasZone = value.endsWith("Z") ||
    value.slice(-6).includes("+") ||
    value.slice(-6).includes("-")
  if (hasZone === false) {
    throw new Error("timestamp requires timezone")
  }
  const stamp = new Date(value)
  if (Number.isNaN(stamp.getTime())) {
    throw new Error("timestamp requires valid value")
  }
  return stamp.toISOString()
}

function records(rows, cutoff) {
  if (rows.length === 0) {
    throw new Error("empty comparison needs separate approval")
  }
  const result = new Map()
  for (const row of rows) {
    const keys = Object.keys(row).sort().join(",")
    const contract = [...FIELDS].sort().join(",")
    if ((keys === contract) === false) {
      throw new Error("record fields do not match contract")
    }
    const key = row["event_id"]
    const validKey = typeof key === "string" && key.length > 0 && result.has(key) === false
    if (validKey === false) {
      throw new Error("missing or duplicate event identity")
    }
    if (Number.isInteger(row["amount_minor"]) === false) {
      throw new Error("amount must be integer minor units")
    }
    for (const field of FIELDS) {
      if (field === "amount_minor") {
        continue
      }
      const validField = typeof row[field] === "string" && row[field].length > 0
      if (validField === false) {
        throw new Error("empty or invalid record field")
      }
    }
    const committed = timestamp(row["committed_at"])
    if (new Date(committed).getTime() > new Date(cutoff).getTime()) {
      throw new Error("record is beyond recovery cutoff")
    }
    const normalized = Object.assign({}, row)
    normalized["committed_at"] = committed
    result.set(key, normalized)
  }
  return result
}

function compare(expected, restored, cutoff) {
  const boundary = timestamp(cutoff)
  const wanted = records(expected, boundary)
  const actual = records(restored, boundary)
  const missing = [...wanted.keys()].filter(key => actual.has(key) === false).sort()
  const unexpected = [...actual.keys()].filter(key => wanted.has(key) === false).sort()
  const changed = [...wanted.keys()].filter(key =>
    actual.has(key) &&
    (JSON.stringify(wanted.get(key)) === JSON.stringify(actual.get(key))) === false
  ).sort()
  return new Map([
    ["pass", missing.length + unexpected.length + changed.length === 0],
    ["missing", missing],
    ["unexpected", unexpected],
    ["changed", changed]
  ])
}

We would pass two lists of records and a timezone-qualified cutoff to compare. A malformed record raises an error; a valid but unequal export returns a failed result with identities to investigate.

We would treat either outcome as a failed comparison. If this function becomes part of a job, its caller must fail on exceptions and on pass values of false.

The code deliberately refuses empty datasets. We would require a separate, documented empty-workflow test instead of allowing two empty exports to produce a reassuring result.

3. Make the Comparison Reject Plausible Errors

We would test the checker before relying on it during backup restore testing. A comparison that has only accepted correct input has not demonstrated useful rejection behavior.

We checked the reference method with synthetic records while preparing this guide. The fixtures contained no customer data, and the exercise did not restore a database.

We would repeat the following cases after adapting the fields to the actual application. The expected results are acceptance criteria for the checker, not claims about a production recovery.

Deliberate Test Case Required Result What the Case Challenges
Identical records in a different order Accept Ordering is not record identity
Missing or substituted event identity Reject The recovered population is wrong
Repeated event identity Reject A duplicate must not disappear during indexing
Accounts exchanged between equal-value records Reject Counts and total value can stay unchanged
One amount increased and another decreased equally Reject An unchanged total can conceal changed records
Changed currency or transaction state Reject Meaningful fields need comparison
Record beyond the cutoff or timestamp without a zone Reject The recovery boundary must be explicit
Empty expected manifest or empty restored export Reject Missing evidence must not pass

We would preserve the test inputs and results with the checker version. A later change to the export schema should trigger a new test, not an undocumented exception.

We would also review what passing cannot establish. The manifest may be incomplete, the selected fields may omit a material fact, or the application may still be unusable.

4. Keep Later External Events Outside the Restore Verdict

We would track events observed by a processor or another external system after the recovery cutoff in a separate reconciliation worklist. They are outside this manifest comparison.

A hypothetical payment accepted externally after the cutoff illustrates the distinction. We would investigate its external identifier and current state before deciding how the recovered application should represent it.

We would not resubmit that payment merely because the restored database lacks it. The restore exercise must not turn missing local history into a second external action.

For each such event, we would record the authoritative source, timestamp, identifier, current status, and accountable reviewer. Recovery can expose reconciliation work without authorizing its execution.

We would leave the pilot on hold if that work changes the balance or state users will see and no approved resolution exists. A technically successful restore does not settle that decision.

Read the Scope Behind Each Backup Verification Result

We inspect the check behind a success label before using it as restore testing evidence. Existing tools provide useful checks, but their scope needs to remain visible.

1. Separate File Checks From Restored Application Checks

We would ask the engineer to show the configured test and its output. We would not infer business usability from the name of a backup product.

Tool or Result Documented Scope Additional Evidence We Would Request
PostgreSQL pg_verifybackup Checks a backup against its manifest, including file integrity checks A real restore and application validation
AWS Backup restore job Restores selected resources and records job timing Custom data validation and workflow acceptance
AWS restore testing validation Runs application-defined validation and reports a result The actual checks and their saved outputs
Veeam SureBackup Test options differ between full recoverability and verification-only modes The selected mode, enabled checks, and application-specific results

We checked these scopes on October 23, 2026, in the PostgreSQL verification guide, AWS restore guide, AWS validation guide, and Veeam guide.

We would keep a checksum result as evidence about the bytes checked. PostgreSQL explicitly cautions that backup verification does not replace test restores.

2. Add Business Validation to the Existing Restore Job

We would attach the agreed business checks to the existing recovery mechanism before building another tool. AWS documents an EventBridge and AWS Lambda pattern for restore testing validation.

The AWS implementation example demonstrates an S3 object-presence check. We would not treat that example as transaction-level validation for a fintech database.

We would record the validation code version, input manifest, result, and failure reason. A handler that reports success without checking those conditions defeats the purpose.

Pro tip: We would retain validation outputs before temporary restore resources are removed. AWS documents cleanup after validation or the configured validation window.

Keep the Recovery Exercise Inside an Isolated Environment

We would isolate the restored application and control its outward actions before exercising the pilot workflow. Restore testing should produce evidence without changing live customer or payment state.

1. Control Outbound Effects

We would ask the engineer to document the restore account or network, credentials, network rules, and destinations. The AWS implementation guide recommends a test or isolated VPC for its RDS example.

We would verify that payment submission, notifications, scheduled jobs, and webhooks use approved test destinations or remain disabled. A label saying “staging” is not evidence of those settings.

We would also document dependencies already supplied by the environment. A restore that borrows a working identity provider has not tested recovery of that identity provider.

2. Exercise the Actual Pilot Workflow

We would have a representative pilot operator complete the agreed workflow using recovered records. Our user acceptance testing discussion explains why a guided demo cannot replace written acceptance criteria.

For a hypothetical transaction-review pilot, we would check that the operator can find a recovered event, inspect its state, and export the expected account view.

We would check permitted and forbidden roles separately. A successful administrator session does not demonstrate what a restricted pilot user can see.

We would retain the expected result and actual output for each journey. Any sandbox-only shortcut belongs in the evidence record so the next reviewer can understand the test's limits.

Make Restore Evidence Part of Custom Software Development Acceptance

We would require a written restore testing result and an explicit acceptance decision before starting the pilot. The record should connect technical evidence to the workflow the practitioner intends to validate.

1. Record the Clock, Scope, and Unresolved Gaps

We would agree the recovery time objective, the tolerable restoration interval, and recovery point objective, the tolerable data-loss interval, with the responsible operator. Microsoft's DR guidance ties those objectives to business priorities.

We would record the exercise start, access preparation, restore completion, validation completion, and operator acceptance. The restore job's elapsed time is only one part of that record.

Evidence Responsible Reviewer Decision It Supports
Backup source and actual recovery point Database engineer The intended copy and boundary were used
Expected manifest and restored export Data owner and engineer Records match the agreed scope
Comparison errors and negative-test results Engineer The checker detects the failures it claims to detect
Application journeys and role checks Pilot operator The recovered workflow is usable for the tested roles
Timing, external-event worklist, and exclusions Accountable operator Remaining gaps are explicit before acceptance

We would preserve the runbook revision and undocumented interventions too. A recovery that depends on an engineer remembering an extra step leaves work for the next rehearsal.

We would treat a new deployment or dependency change as a reason to reassess the exercise. Our production reliability article covers the related problem of changing systems safely.

2. Configure Existing Tools Before Adding Software

We would start with the backup platform, export mechanism, and evidence store already available. The missing piece may be an acceptance rule or a named owner.

We would consider custom software development when repeated workflow gaps justify maintaining another component. A reusable comparison job can help, but it also needs ownership, tests, and schema-change handling.

We would keep the checker and runbook accessible to the team that will operate the pilot. Our code ownership discussion explains why practical access matters alongside the agreement.

We would not build a dashboard just to display a green result. The useful output is evidence that another reviewer can inspect, reproduce, and reject when conditions change.

Rehearse the Failure Before Accepting the Pilot

We would make restore testing a concrete acceptance exercise by agreeing the boundary, recovering in isolation, comparing meaningful records, testing rejection behavior, and validating the intended workflow.

For custom software development, we would retain failures and exclusions alongside successful checks. Then we would retest the corrected procedure before inviting users into the pilot.

We can discuss the recovery evidence your pilot needs at Better Software.

Frequently Asked Questions

What is DRP testing?

We use DRP testing to check a written disaster recovery plan, its roles, and its steps. Acronis lists several test methods. We would pick a test based on the failure at hand, with a clear scope and people assigned to act.

What is the 3/2/1 rule for backing up?

We use Backblaze's definition of the 3/2/1 rule as three copies of data, on two media types, with one copy off-site. The production copy counts toward the three. We treat this as a copy-placement strategy because it does not specify the scope of a restore testing exercise.

What is BCP vs DRP?

We use a business continuity plan to keep core work going during a disruption. A disaster recovery plan sets out how to recover systems. Microsoft separates these scopes. We would align both plans with the same business needs and owners.

Which backup type is fastest to restore?

We would time a real restore before choosing a backup type for speed. AWS compares full, incremental, and differential backups, each with different restore needs. We would test the storage source and transfer path too, because the backup type alone cannot settle the choice.

When would a DRP be activated?

We would activate the plan when its documented incident criteria are met and the designated decision-maker authorizes it. Microsoft's guidance calls for clear activation criteria. We would agree those criteria beforehand, including who can decide when the usual approver is unavailable.

About the author

Better Software

Better Software Team

Product and engineering team

Better Software Team is the product and engineering team at Better Software. We build new software products and improve existing applications and systems, with deeper experience in healthcare, finance and energy.