Customer 360
Unify a single customer identity ("John Doe") across 4 source systems with email and name variations into one golden record.
The Problem
John Doe is a real customer. He exists in your Stripe billing system, Salesforce CRM, HubSpot marketing platform, and an internal PostgreSQL database. Each system captured his information at different times, through different forms, with different data quality standards. The result: four records that all represent the same person but look different enough to defeat naive deduplication.
Your sales team sends him duplicate outreach. Your billing system cannot reconcile his subscription with his CRM record. Your support team creates a new ticket profile every time he contacts a different channel.
The Data
Stripe (Billing)
| Field | Value |
|---|---|
| Source | stripe |
| ID | cus_N8x4kLm2pQ |
| Name | John Doe |
| [email protected] | |
| Phone | +1-555-0142 |
| Company | null |
| Last Updated | 2026-01-18 |
Salesforce (CRM)
| Field | Value |
|---|---|
| Source | salesforce |
| ID | 003Dn00000Lx8FQIAZ |
| Name | Jonathan Doe |
| [email protected] | |
| Phone | +1-555-0142 |
| Company | Acme Corporation |
| Last Updated | 2026-01-10 |
HubSpot (Marketing)
| Field | Value |
|---|---|
| Source | hubspot |
| ID | hub_29847163 |
| Name | J. Doe |
| [email protected] | |
| Phone | null |
| Company | Acme Corp |
| Last Updated | 2025-12-22 |
Internal Database
| Field | Value |
|---|---|
| Source | internal |
| ID | usr_00419 |
| Name | John D. |
| [email protected] | |
| Phone | +1-555-0142 |
| Company | null |
| Last Updated | 2025-11-30 |
Summary View
| Source | ID | Name | Phone | Company | Last Updated | |
|---|---|---|---|---|---|---|
| Stripe | cus_N8x4kLm2pQ | John Doe | [email protected] | +1-555-0142 | null | 2026-01-18 |
| Salesforce | 003Dn00000Lx8FQIAZ | Jonathan Doe | [email protected] | +1-555-0142 | Acme Corporation | 2026-01-10 |
| HubSpot | hub_29847163 | J. Doe | [email protected] | null | Acme Corp | 2025-12-22 |
| Internal | usr_00419 | John D. | [email protected] | +1-555-0142 | null | 2025-11-30 |
Challenges in this data:
- Name variations: "John Doe", "Jonathan Doe", "J. Doe", "John D.", four representations of the same person
- Email variations:
[email protected],[email protected],[email protected], three different email addresses - Missing fields: HubSpot has no phone; Stripe and Internal have no company
- Company variations: "Acme Corporation" vs "Acme Corp" vs null
No single field uniquely identifies this customer across all four systems. Identity resolution requires combining multiple signals.
The Spec
api_version: kanoniv/v1
identity_version: "1.0"
entity:
name: customer
description: "Unified customer identity across billing, CRM, marketing, and internal systems"
sources:
- name: salesforce
adapter: csv
location: data/salesforce_contacts.csv
primary_key: contact_id
attributes:
name: name
email: email
phone: phone
company: company
last_updated: last_updated
- name: stripe
adapter: csv
location: data/stripe_customers.csv
primary_key: customer_id
attributes:
name: name
email: email
phone: phone
company: company
last_updated: last_updated
- name: hubspot
adapter: csv
location: data/hubspot_contacts.csv
primary_key: hub_id
attributes:
name: name
email: email
phone: phone
company: company
last_updated: last_updated
- name: internal
adapter: csv
location: data/internal_users.csv
primary_key: user_id
attributes:
name: name
email: email
phone: phone
company: company
last_updated: last_updated
rules:
# Email is the strongest signal. Even though John has 3 different
# email addresses, two systems share [email protected] -- that alone
# is enough to link Stripe and HubSpot.
- name: email_exact
type: exact
field: email
weight: 1.0
# Fuzzy name matching catches the variations. "John Doe" vs
# "Jonathan Doe" scores ~0.87 in Jaro-Winkler (prefix "Jo" helps).
# "J. Doe" vs "John Doe" scores ~0.82 -- above our 0.85 threshold
# when combined with the shared prefix weighting.
- name: name_fuzzy
type: similarity
field: name
algorithm: jaro_winkler
threshold: 0.85
weight: 0.6
# Phone is a strong corroborating signal. Three of four systems
# share +1-555-0142. This bridges records that don't share an email.
- name: phone_exact
type: exact
field: phone
weight: 0.8
blocking:
strategy: exact
keys: [email, phone]
decision:
scoring: weighted_sum
thresholds:
match: 0.85
review: 0.6
survivorship:
default: source_priority
overrides:
- field: email
strategy: most_recent
- field: last_updated
strategy: aggregate
function: max
metadata:
owner: [email protected]
tags: [customer-360, production]
description: "Customer 360 resolution across 4 source systems"Why These Rules Work
| Record Pair | email_exact | name_fuzzy | phone_exact | Total Score | Decision |
|---|---|---|---|---|---|
| Stripe / HubSpot | 1.0 ([email protected]) | 0.6 (John Doe vs J. Doe) | 0.0 (null phone) | 1.6 | Match |
| Stripe / Salesforce | 0.0 (different emails) | 0.6 (John Doe vs Jonathan Doe) | 0.8 (+1-555-0142) | 1.4 | Match |
| Stripe / Internal | 0.0 (different emails) | 0.6 (John Doe vs John D.) | 0.8 (+1-555-0142) | 1.4 | Match |
| Salesforce / HubSpot | 0.0 (different emails) | 0.6 (Jonathan Doe vs J. Doe) | 0.0 (null phone) | 0.6 | Review |
| Salesforce / Internal | 0.0 (different emails) | 0.6 (Jonathan Doe vs John D.) | 0.8 (+1-555-0142) | 1.4 | Match |
| HubSpot / Internal | 0.0 (different emails) | 0.6 (J. Doe vs John D.) | 0.0 (null phone) | 0.6 | Review |
All six pairs score above the review threshold. Five of six score above the match threshold. The engine clusters all four records into a single canonical entity through transitive closure, even though no single rule connects all four directly.
The Result
Golden Record
After survivorship resolution, the canonical entity contains:
| Field | Value | Source | Reason |
|---|---|---|---|
| Name | Jonathan Doe | Salesforce | source_priority: Salesforce is first in source order |
| [email protected] | Stripe | most_recent override: Stripe updated Jan 18 (most recent) | |
| Phone | +1-555-0142 | Salesforce | source_priority: Salesforce is first and non-null |
| Company | Acme Corporation | Salesforce | source_priority: Salesforce is first and non-null |
| Last Updated | 2026-01-18 | Stripe | aggregate(max): latest timestamp across all sources |
Cluster Visualization
Canonical Entity: canon_8f3a
├── salesforce/003Dn00000Lx8FQIAZ (Jonathan Doe, [email protected])
│ ↕ phone_exact (0.8) + name_fuzzy (0.6)
├── stripe/cus_N8x4kLm2pQ (John Doe, [email protected])
│ ↕ email_exact (1.0) + name_fuzzy (0.6)
├── hubspot/hub_29847163 (J. Doe, [email protected])
│ ↕ phone_exact (0.8) + name_fuzzy (0.6)
└── internal/usr_00419 (John D., [email protected])Field Provenance
The API response includes field_provenance showing exactly which source contributed each field to the golden record:
{
"canonical_id": "canon_8f3a",
"entity_type": "customer",
"canonical_data": {
"name": "Jonathan Doe",
"email": "[email protected]",
"phone": "+1-555-0142",
"company": "Acme Corporation",
"last_updated": "2026-01-18"
},
"field_provenance": {
"name": { "source": "salesforce", "record_id": "003Dn00000Lx8FQIAZ", "strategy": "source_priority" },
"email": { "source": "stripe", "record_id": "cus_N8x4kLm2pQ", "strategy": "most_recent" },
"phone": { "source": "salesforce", "record_id": "003Dn00000Lx8FQIAZ", "strategy": "source_priority" },
"company": { "source": "salesforce", "record_id": "003Dn00000Lx8FQIAZ", "strategy": "source_priority" },
"last_updated": { "source": "stripe", "record_id": "cus_N8x4kLm2pQ", "strategy": "aggregate_max" }
},
"cluster": {
"size": 4,
"sources": ["stripe", "salesforce", "hubspot", "internal"],
"confidence": 0.94
}
}Reconciliation Summary
Sources: 4
Total records: 4
Clusters: 1
Golden records: 1
Merge rate: 75.0% (4 records → 1 golden record)
Match pairs: 5
Review pairs: 1
Rejected pairs: 0Key Takeaway
Source priority survivorship ensures your most trusted system wins. By declaring Salesforce first in the sources list, CRM data takes precedence for name and company fields, even though Stripe has newer data. The most_recent override on the email field ensures the freshest contact information is preserved regardless of source rank.
This pattern is the foundation for most Customer 360 implementations: pick a system of record for identity fields, but allow per-field overrides for data that changes frequently.
Variations to Consider
If you trust the most recent data overall, replace source_priority with most_recent:
survivorship:
default: most_recentIf name matching produces false positives (common with short names), raise the threshold:
- name: name_fuzzy
type: similarity
field: name
algorithm: jaro_winkler
threshold: 0.92 # stricter threshold
weight: 0.6If you want company name to corroborate matches, add a fuzzy company rule:
- name: company_fuzzy
type: similarity
field: company
algorithm: jaro_winkler
threshold: 0.90
weight: 0.3Related
- Spec Reference: Sources: Adapter configuration for all source types
- Spec Reference: Survivorship: All survivorship strategies and per-field overrides
- Spec Reference: Rules: Algorithm comparison and weight guidelines
- Tutorial: Customer 360: Step-by-step walkthrough building this from scratch
