Skip to main content

Business Application Migration

Business application migration transfers organisational data, configurations, and integrations from a source system to a target platform. This task covers migrations of CRM, ERP, human capital management, case management, and grants management systems. Execute this procedure when replacing a business application with a new platform, consolidating multiple systems into one, or moving from on-premises to cloud deployment.

The migration produces a fully operational target system containing all historical data, configured integrations, and trained users. Source systems remain available during parallel running before final decommissioning.

Prerequisites

Complete these requirements before beginning migration work. Missing prerequisites cause delays during critical cutover periods when recovery options are limited.

Target system readiness

RequirementSpecificationVerification
Target platform provisionedProduction and UAT environments deployedLogin to both environments succeeds
Administrator accessFull admin rights on target systemCan create users, modify settings
Integration credentialsAPI keys, service accounts for all integrationsAuthentication test passes for each
Storage capacity3x current data volume for migration workspaceCapacity report shows headroom
Licence allocationLicences for all migrating users plus 10% bufferLicence count confirmed with vendor

Source system access

RequirementSpecificationVerification
Export permissionsRights to extract all data including attachmentsTest export of sample records succeeds
Schema documentationComplete field definitions for all objectsDocumentation matches actual schema
Integration inventoryList of all systems connected to sourceEach integration owner identified
Historical data scopeDecision on how many years of data to migrateDocumented and approved by data owner

Data mapping completion

The data mapping document defines how each source field transforms into target fields. Complete mappings for all object types before extraction begins. A typical CRM migration involves 40 to 80 field mappings per object type; ERP migrations involve 200 to 500 mappings across financial objects.

Example mapping excerpt for contact records:

Source fieldSource typeTarget fieldTarget typeTransformation
CONTACT_IDVARCHAR(20)external_idText(50)Prefix with ‘LEG-‘
FIRST_NMVARCHAR(50)first_nameText(100)Trim whitespace
LAST_NMVARCHAR(50)last_nameText(100)Trim whitespace
EMAIL_ADDRVARCHAR(100)emailEmailValidate format, null if invalid
PHONE_1VARCHAR(20)phonePhoneNormalise to E.164
CREATED_DTDATElegacy_createdDateTimeAppend ‘T00:00:00Z’
STATUS_CDCHAR(1)statusPicklistMap: A→Active, I→Inactive, D→Deleted

Team and schedule

RoleResponsibilityTime commitment
Migration leadOverall coordination, issue resolution100% during migration
Source system adminExtraction, source queries50% during extraction, on-call during cutover
Target system adminLoading, configuration50% during load phases, 100% during cutover
Integration developerAPI work, connection testing25% ongoing, 100% during integration cutover
Data steward per objectValidation, exception handling20% during validation phases
Training coordinatorUser preparation, support50% in final two weeks

Reserve a cutover weekend with change approval at least four weeks in advance. Standard cutover windows span Friday 18:00 to Monday 06:00, providing 60 hours for execution and rollback if needed.

Procedure

The migration follows five phases: extraction, transformation, loading, validation, and cutover. Each phase has defined entry and exit criteria. Do not proceed to the next phase until exit criteria are met.

+----------------------------------------------------------------+
| MIGRATION PHASES |
+----------------------------------------------------------------+
| |
| +------------+ +------------+ +------------+ |
| | | | | | | |
| | EXTRACTION +--->+ TRANSFORM +--->+ LOAD | |
| | | | | | | |
| | Week 1-2 | | Week 2-3 | | Week 3-4 | |
| +------------+ +------------+ +------+-----+ |
| | |
| +-------------------------------------+ |
| | |
| v |
| +------------+ +------------+ |
| | | | | |
| | VALIDATION +--->+ CUTOVER | |
| | | | | |
| | Week 4-5 | | Weekend | |
| +------------+ +------------+ |
| |
+----------------------------------------------------------------+

Phase 1: Data extraction

  1. Create extraction workspace on a system with network access to the source database and sufficient storage. For a source system containing 500,000 records with attachments, allocate 100 GB minimum.
Terminal window
mkdir -p /data/migration/{extracts,transforms,logs}
df -h /data/migration
# Confirm available space exceeds 100 GB
  1. Extract reference data first. Reference data includes picklist values, status codes, user lists, and configuration tables that other records depend on. Export to CSV with UTF-8 encoding and Unix line endings.
-- Example: Extract status codes from source
SELECT status_code, status_name, is_active, sort_order
FROM ref_status_codes
WHERE object_type = 'CONTACT'
ORDER BY sort_order;

Save output to /data/migration/extracts/ref_status_codes.csv. Record row counts in the extraction log.

  1. Extract master data objects in dependency order. Master data includes entities that exist independently: organisations, contacts, users, products, accounts. Extract parent objects before children.

    Typical dependency order for CRM:

    • Organisations (no dependencies)
    • Contacts (depends on organisations)
    • Opportunities (depends on organisations, contacts)
    • Activities (depends on opportunities, contacts)

    For each object, execute a full extract with all fields:

SELECT *
FROM contacts
WHERE created_date >= '2019-01-01' -- Per historical scope decision
ORDER BY contact_id;
  1. Extract transactional data after master data. Transactional data includes records created through business processes: invoices, payments, case notes, grant transactions. These have complex dependencies on master data.

    For ERP financial data, extract in fiscal period order:

SELECT *
FROM gl_transactions
WHERE fiscal_year >= 2020
ORDER BY fiscal_year, fiscal_period, transaction_date;
  1. Extract attachments and documents separately. Large binary objects slow extraction and require different handling. Create a manifest linking attachments to parent records.
Terminal window
# Generate attachment manifest
psql -c "SELECT attachment_id, parent_type, parent_id, filename,
file_size, mime_type
FROM attachments" > /data/migration/extracts/attachment_manifest.csv
# Export binary files
pg_dump --data-only --table=attachment_content \
--file=/data/migration/extracts/attachments.dump
  1. Validate extraction completeness. Compare extracted row counts against source system counts. Discrepancies over 0.1% require investigation before proceeding.
-- Source system counts
SELECT 'contacts' as object, COUNT(*) as cnt FROM contacts
WHERE created_date >= '2019-01-01'
UNION ALL
SELECT 'organisations', COUNT(*) FROM organisations
WHERE created_date >= '2019-01-01';

Document counts in extraction log with timestamp and extractor name.

Phase 1 exit criteria: All objects extracted, row counts documented and verified within 0.1% of source counts, extraction log complete.

Phase 2: Data transformation

Transformation converts extracted data into the target system’s format. This phase executes the data mapping document and handles data quality issues.

  1. Set up the transformation environment. Use a scripting language with strong data handling capabilities. Python with pandas handles most transformations; complex ERP migrations benefit from dedicated ETL tools.
import pandas as pd
from pathlib import Path
EXTRACT_DIR = Path('/data/migration/extracts')
TRANSFORM_DIR = Path('/data/migration/transforms')
# Load reference data first
status_map = pd.read_csv(EXTRACT_DIR / 'ref_status_codes.csv')
status_lookup = dict(zip(status_map['status_code'],
status_map['status_name']))
  1. Apply field-level transformations according to the mapping document. Handle each transformation type systematically:

    Direct copy transfers values unchanged. Use for fields with matching types and no business logic differences.

    Type conversion changes data types. Convert dates to ISO 8601, normalise phone numbers to E.164, validate email formats.

def transform_contacts(df):
result = pd.DataFrame()
# Direct copy with rename
result['external_id'] = 'LEG-' + df['CONTACT_ID'].astype(str)
# Trim whitespace
result['first_name'] = df['FIRST_NM'].str.strip()
result['last_name'] = df['LAST_NM'].str.strip()
# Validate and transform email
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
result['email'] = df['EMAIL_ADDR'].where(
df['EMAIL_ADDR'].str.match(email_pattern),
other=None
)
# Normalise phone to E.164
result['phone'] = df['PHONE_1'].apply(normalise_phone)
# Map status codes
result['status'] = df['STATUS_CD'].map(
{'A': 'Active', 'I': 'Inactive', 'D': 'Deleted'}
)
return result

Value mapping translates source values to target equivalents. Picklists, status codes, and categorical fields require explicit mapping tables.

Derived fields calculate new values from source data. Common derivations include full names from components, fiscal periods from dates, and categorisations from business rules.

  1. Handle data quality exceptions. Transformation reveals data quality issues hidden in the source system. Create exception reports rather than failing the entire transformation.
def transform_with_exceptions(df, transform_func, object_name):
exceptions = []
results = []
for idx, row in df.iterrows():
try:
results.append(transform_func(row))
except Exception as e:
exceptions.append({
'object': object_name,
'source_id': row.get('id', idx),
'error': str(e),
'row_data': row.to_dict()
})
# Write exceptions for review
if exceptions:
pd.DataFrame(exceptions).to_csv(
TRANSFORM_DIR / f'{object_name}_exceptions.csv',
index=False
)
return pd.DataFrame(results)

Review exceptions with data stewards. Classify each exception as:

  • Fix in source: Correct the data in source system, re-extract
  • Fix in transformation: Add handling logic for edge case
  • Exclude: Do not migrate this record (document reason)
  • Manual load: Handle outside automated process
  1. Generate transformation audit trail. For each record, capture source ID, target ID, transformation timestamp, and any applied rules. The audit trail enables tracing discrepancies post-migration.
audit_records = []
for source_row, target_row in zip(source_df.itertuples(),
target_df.itertuples()):
audit_records.append({
'source_id': source_row.CONTACT_ID,
'target_external_id': target_row.external_id,
'transform_timestamp': datetime.utcnow().isoformat(),
'rules_applied': 'standard_contact_transform_v2'
})
pd.DataFrame(audit_records).to_csv(
TRANSFORM_DIR / 'contacts_audit.csv',
index=False
)
  1. Validate transformed data against target schema. Before loading, verify all required fields are populated, field lengths fit target constraints, and foreign key relationships are satisfiable.
def validate_for_target(df, schema):
errors = []
for field, spec in schema.items():
if spec['required'] and df[field].isna().any():
errors.append(f"{field}: {df[field].isna().sum()} null values")
if spec['max_length']:
over_length = df[field].str.len() > spec['max_length']
if over_length.any():
errors.append(
f"{field}: {over_length.sum()} values exceed "
f"{spec['max_length']} chars"
)
return errors

Phase 2 exit criteria: All objects transformed, exception rate below 2%, exceptions reviewed and dispositioned, audit trail complete, schema validation passes.

Phase 3: Data loading

Loading imports transformed data into the target system. Execute loads in UAT first, then production after validation passes.

  1. Prepare the target environment for bulk loading. Disable automated workflows, email notifications, and validation rules that would fire during import. Document everything disabled for re-enablement after loading.
Disabled for migration load:
- Workflow: New Contact Welcome Email (disabled 2024-03-15 09:00)
- Workflow: Opportunity Stage Change Notification (disabled 2024-03-15 09:00)
- Validation: Email Domain Check (disabled 2024-03-15 09:05)
- Trigger: Contact Duplicate Check (disabled 2024-03-15 09:05)
  1. Load reference data first. Reference data must exist before dependent records can reference it. Use the target system’s import API or bulk loader.
Terminal window
# Example: Load status values via API
curl -X POST https://target-system.example.org/api/v2/picklists/status \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d @/data/migration/transforms/ref_status_codes.json
  1. Load master data in dependency order. Parent objects before children. Use batch operations to improve throughput; typical batch sizes range from 200 to 2,000 records depending on record complexity and API limits.
import requests
from time import sleep
def load_batch(records, endpoint, batch_size=500):
results = {'success': 0, 'failed': 0, 'errors': []}
for i in range(0, len(records), batch_size):
batch = records[i:i + batch_size]
response = requests.post(
f'https://target-system.example.org/api/v2/{endpoint}/batch',
headers={'Authorization': f'Bearer {API_TOKEN}'},
json={'records': batch}
)
if response.status_code == 200:
batch_result = response.json()
results['success'] += batch_result['created']
results['failed'] += len(batch_result.get('errors', []))
results['errors'].extend(batch_result.get('errors', []))
else:
results['failed'] += len(batch)
results['errors'].append({
'batch_start': i,
'error': response.text
})
# Respect rate limits
sleep(1)
return results
  1. Capture ID mappings during load. The target system generates new internal IDs. Map these back to source IDs for relationship linking and post-migration tracing.
id_mapping = {}
for source_id, target_response in load_results:
id_mapping[source_id] = target_response['id']
# Save mapping for relationship linking
pd.DataFrame([
{'source_id': k, 'target_id': v}
for k, v in id_mapping.items()
]).to_csv(TRANSFORM_DIR / 'contacts_id_mapping.csv', index=False)
  1. Load child objects using ID mappings to resolve foreign keys. Replace source system foreign keys with target system IDs before loading.
# Load opportunities referencing contacts
opportunities_df = pd.read_csv(
TRANSFORM_DIR / 'opportunities_transformed.csv'
)
# Map contact foreign keys
contact_id_map = pd.read_csv(
TRANSFORM_DIR / 'contacts_id_mapping.csv'
).set_index('source_id')['target_id'].to_dict()
opportunities_df['contact_id'] = (
opportunities_df['source_contact_id'].map(contact_id_map)
)
# Handle unmapped references
unmapped = opportunities_df['contact_id'].isna()
if unmapped.any():
print(f"Warning: {unmapped.sum()} opportunities have unmapped contacts")
# Log for manual review
  1. Load attachments after parent records exist. Stream large files rather than loading entirely into memory.
def load_attachment(file_path, parent_type, parent_id):
with open(file_path, 'rb') as f:
response = requests.post(
f'https://target-system.example.org/api/v2/{parent_type}/'
f'{parent_id}/attachments',
headers={'Authorization': f'Bearer {API_TOKEN}'},
files={'file': f}
)
return response.status_code == 201
  1. Re-enable disabled components after loading completes. Test each component functions correctly with migrated data before proceeding.

Phase 3 exit criteria: All records loaded to UAT, load error rate below 1%, ID mappings complete, disabled components re-enabled, basic functionality verified.

Phase 4: Validation

Validation confirms migrated data is complete, accurate, and usable. Execute validation in UAT before production cutover.

  1. Perform record count reconciliation. Compare counts in target system against source system and transformation output.

    ObjectSource countTransformedLoadedVariance
    Contacts45,23045,18045,180-0.11%
    Organisations12,45012,45012,4500.00%
    Opportunities8,9208,9158,915-0.06%
    Activities234,500234,420234,418-0.03%

    Investigate variances exceeding 0.1%. Common causes: date range filtering, duplicate removal, exception exclusions.

  2. Execute field-level validation on a statistical sample. For objects with over 10,000 records, validate a random sample of 500 records. Compare field values between source and target.

import random
sample_ids = random.sample(list(contact_id_map.keys()), 500)
validation_results = []
for source_id in sample_ids:
target_id = contact_id_map[source_id]
source_record = fetch_from_source(source_id)
target_record = fetch_from_target(target_id)
for field_mapping in mapping_document:
source_val = source_record.get(field_mapping['source_field'])
target_val = target_record.get(field_mapping['target_field'])
expected = apply_transform(source_val, field_mapping)
validation_results.append({
'source_id': source_id,
'field': field_mapping['target_field'],
'expected': expected,
'actual': target_val,
'match': expected == target_val
})
# Calculate pass rate
pass_rate = sum(r['match'] for r in validation_results) / len(validation_results)
print(f"Field validation pass rate: {pass_rate:.2%}")

Target pass rate: 99.5% or higher. Lower rates indicate systematic transformation errors.

  1. Validate referential integrity. Confirm all foreign key relationships resolve correctly in the target system.
-- Find orphaned opportunity records
SELECT o.id, o.external_id, o.contact_id
FROM opportunities o
LEFT JOIN contacts c ON o.contact_id = c.id
WHERE c.id IS NULL;
-- Expected: 0 rows
  1. Verify calculated fields and aggregations. Business logic in the target system may calculate totals, statuses, or derived values. Compare against source system values.
-- Compare organisation opportunity totals
SELECT
o.external_id,
o.opportunity_total as migrated_total,
(SELECT SUM(amount) FROM opportunities
WHERE organisation_id = o.id) as calculated_total
FROM organisations o
WHERE o.opportunity_total != (
SELECT SUM(amount) FROM opportunities
WHERE organisation_id = o.id
);
  1. Conduct user acceptance testing. Provide data stewards and key users access to UAT. Supply test scripts covering common workflows:

    • Find a specific contact by name and verify details
    • Open an organisation and verify related records
    • Generate a report matching a known source system report
    • Execute a search matching known criteria

    Document test results with screenshots. Obtain sign-off from each data steward.

  2. Validate integrations with test transactions. Each integration point requires end-to-end testing in UAT.

Integration test: Finance system sync
1. Create test transaction in target CRM: Invoice INV-TEST-001, £1,500.00
2. Trigger sync to finance system
3. Verify transaction appears in finance system within 5 minutes
4. Verify amount, date, reference match
5. Delete test transaction from both systems
Result: PASS / FAIL
Tester: _______________
Date: _______________

Phase 4 exit criteria: Count reconciliation within 0.1%, field validation above 99.5%, zero orphaned records, user acceptance sign-off obtained, integration tests pass.

Phase 5: Production cutover

Cutover migrates production data and transitions users to the target system. Execute during the reserved change window.

+---------------------------------------------------------------------+
| CUTOVER SEQUENCE |
+---------------------------------------------------------------------+
| |
| Friday 18:00 Saturday Sunday Monday |
| | | | | |
| +----v----+ +----v----+ +----v----+ +----v----+ |
| | Source | | Final | | Go-Live | | Support | |
| | freeze | | load | | verify | | & hyper | |
| | | | | | | | care | |
| +---------+ +---------+ +---------+ +---------+ |
| |
| 18:00 Freeze 00:00 Delta 06:00 UAT 06:00 Users |
| 19:00 Delta 06:00 Complete 12:00 Prod access |
| extract 08:00 Validate 18:00 Go/NoGo production |
| |
+---------------------------------------------------------------------+
  1. Communicate cutover start to all stakeholders. Send notification 24 hours before and again at freeze time.
Subject: CRM Migration - System freeze starting Friday 18:00
The CRM system will be READ-ONLY from Friday 18:00 until Monday 06:00.
During this time:
- You can view existing records
- You cannot create or edit records
- Email logging is paused (emails will be logged after migration)
On Monday morning, access the new system at: https://crm.example.org
Training materials: https://intranet.example.org/crm-training
Support contact: it-support@example.org or ext. 2200
  1. Freeze the source system for write operations. Configure read-only mode or revoke write permissions. Verify no writes occur after freeze time.
-- Verify no records modified after freeze
SELECT COUNT(*)
FROM contacts
WHERE modified_date > '2024-03-22 18:00:00';
-- Expected: 0
  1. Extract delta records created since the last full extraction. Use modification timestamps to identify changes.
SELECT * FROM contacts
WHERE modified_date > '2024-03-15 00:00:00' -- Last full extract
ORDER BY contact_id;

Transform and load delta records using the same process as the full migration.

  1. Execute final data synchronisation. Run incremental extraction, transformation, and loading for all changed records. Typical delta volumes are 5% to 15% of full dataset.

  2. Perform production validation. Execute abbreviated validation suite on production data:

    • Record count reconciliation (all objects)
    • Sample validation (100 records per object type)
    • Critical report comparison
    • Integration connectivity test
  3. Make go/no-go decision. At the scheduled decision point, assess readiness:

    CriterionThresholdActualStatus
    Count variance< 0.1%0.03%PASS
    Sample accuracy> 99.5%99.8%PASS
    Critical integrations100%100%PASS
    Blocking defects00PASS
    Rollback testedYesYesPASS

    GO: All criteria met. Proceed to go-live. NO-GO: Any criterion failed. Execute rollback, schedule new window.

  4. Enable user access to production. Update DNS, distribute credentials, or enable SSO integration. Remove access to source system write functions.

  5. Monitor hypercare period. Provide enhanced support for 5 business days post-migration:

    • Dedicated support queue for migration issues
    • Data stewards available for exception review
    • Daily defect triage with migration team
    • Twice-daily status updates to stakeholders

Rollback window

Maintain rollback capability for 48 hours post-cutover. After 48 hours, new data in the target system makes rollback impractical without data loss. Communicate this constraint to decision-makers.

Post-migration activities

Complete these activities within 30 days of cutover:

  1. Resolve exception backlog: Process all records flagged during migration. Target: zero open exceptions within 14 days.

  2. Decommission source system: After confirming target system stability, archive source system data and decommission infrastructure. Follow System Decommissioning procedures.

  3. Update documentation: Revise integration documentation, system diagrams, and operational runbooks to reflect the target system.

  4. Conduct lessons learned: Document what worked, what failed, and recommendations for future migrations.

Verification

Confirm successful migration through these verification steps:

Record completeness verification:

-- Target system record counts
SELECT
'contacts' as object, COUNT(*) as count FROM contacts
UNION ALL SELECT
'organisations', COUNT(*) FROM organisations
UNION ALL SELECT
'opportunities', COUNT(*) FROM opportunities;

Compare against documented source counts. Variance should not exceed 0.1% (excluding documented exclusions).

Data accuracy verification:

Select 50 records at random and manually compare source and target field values. All transformed fields should match expected values based on mapping rules. Document any discrepancies with explanation.

Integration verification:

IntegrationTest actionExpected resultActual result
Finance syncCreate invoiceAppears in finance system within 5 min
Email loggingSend test emailLogged against correct contact
Marketing platformUpdate contactSyncs within 15 min
Reporting warehouseRun daily extractIncludes migrated records

User functionality verification:

Have three users from different teams execute their standard workflows and confirm:

  • Records display correctly
  • Searches return expected results
  • Reports generate accurate data
  • Workflow triggers function

Troubleshooting

SymptomCauseResolution
Load fails with “duplicate external ID”External ID already exists from previous load attemptClear failed records from target, implement idempotent load logic
Foreign key constraint violation during loadChild records loaded before parentsReorder load sequence, verify dependency order
Character encoding errors in transformed dataSource encoding differs from UTF-8Specify source encoding in extraction, convert explicitly during transformation
API rate limit exceeded during loadBatch size too large or no delay between batchesReduce batch size to 200, add 2-second delay between batches
Missing attachments in target systemAttachment load ran before parent records existedReload attachments after parent records confirmed, verify parent ID mapping
Date values shifted by one dayTimezone conversion issueStandardise on UTC throughout migration, apply timezone only at display
Decimal precision loss in financial dataFloat type used instead of decimalUse decimal/numeric types, validate sum totals match
Lookup field shows “not found”Reference data not loaded or ID mapping incorrectVerify reference data exists, check ID mapping table
Users cannot find records by old IDExternal ID search not configuredAdd external ID to search index, train users on new search
Integration fails with authentication errorService account credentials not updated for target systemUpdate integration credentials, verify OAuth tokens if applicable
Performance degradation in target systemMissing indexes, suboptimal queries from migration data patternsAnalyse slow queries, add indexes, rebuild statistics
Reports show different totals than sourceAggregation logic differs between systems, or data excluded during migrationCompare SQL logic, reconcile excluded records, adjust report definitions
Workflow firing on migrated recordsWorkflow evaluates on record creation, not migration flagAdd migration flag field, filter workflows to exclude migrated records
Users see records they should not accessSharing rules not migrated or differ between systemsReview sharing model, manually configure record access, bulk update owner assignments
Historical dates display incorrectlyDate format interpretation differsVerify ISO 8601 format used, check locale settings in target

See also