We're moving from Salesforce to HubSpot. The business case was cost and the sales team wanted the simpler UI. The technical requirement was migrating 15+ custom object types with complex relationships and ~500k records total. API timeouts and data integrity were the main obstacles.
The migration pattern is extract-transform-load, but you need to handle the dependencies. We used a three-phase approach:
1. **Schema Mapping & Dependency Graph:** You can't just export `CustomObject__c` tables in random order. We used Python to map the schema and build a dependency graph to determine load order.
```python
# Simplified dependency mapping logic
dependencies = {
'OpportunityLineItem': ['Opportunity', 'Product2'],
'Opportunity': ['Account', 'Contact'],
# ... etc
}
# Topological sort to get load order
```
2. **Extract with Pagination & State:** Salesforce bulk API v2 with proper job state management. We stored each batch's success/failure logs in S3 for replayability.
```bash
# Example using sfdx for extraction, but we used custom scripts
sfdx force:data:tree:export -q "SELECT Id, Name FROM CustomObject__c"
```
3. **Load with Idempotency & Retry:** HubSpot's APIs have lower rate limits. We implemented exponential backoff and used a mapping table (`old_sf_id -> new_hs_id`) to maintain relationships during insertion. This is non-negotiable.
**Cutover Plan:**
* **Week 1:** Final schema sync, load all static/reference data (Products, Territories).
* **Week 2:** Full historical data load for closed/won opportunities and accounts. Ran reconciliation reports.
* **Cutover Weekend:** Paused syncs Friday 5 PM. Ingested all open opportunities and activities (last 90 days). Monday 6 AM: flipped sales team access to HubSpot, read-only mode in Salesforce for historical lookup.
**Timeline & Gotchas:**
Total project: 8 weeks (2 people). The actual data migration scripts took 3 weeks to write and test. The biggest time sink was handling picklist value mismatches and field length differences. The final cutover data sync took 14 hours due to unexpected API throttling—always build in a 50% time buffer for the final run.
Key metrics: 99.8% record fidelity post-migration. 0.2% were legacy attachments with invalid formats that we archived separately. Log everything; you will need it for the audit.
-shift
shift left or go home
That dependency graph step is so crucial, we learned that the hard way on a smaller migration. The topological sort saved us when we had circular references between two custom objects.
How did you handle record ownership mapping during the load phase? That's where we saw the most data corruption, especially when user IDs didn't exist in the new system yet.
MartechStruggles