---
# === IDENTITY ===
id: business/erp-integration/canonical-data-model-design/2026
canonical_question: "How do you design a canonical data model for multi-system ERP integration?"
aliases:
  - "Canonical data model design for multi-ERP integration"
  - "How to build a shared data model across SAP, Salesforce, NetSuite, and Dynamics 365"
  - "Enterprise canonical schema design patterns and versioning"
  - "Canonical model vs point-to-point mapping for ERP integration"
entity_type: erp_integration
domain: business > erp-integration > canonical-data-model-design
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === SYSTEM PROFILE ===
# Architecture pattern card — applies across all major ERPs.
systems:
  - name: "SAP S/4HANA"
    vendor: "SAP"
    version: "2023+ (OData v4)"
    edition: "Enterprise"
    deployment: hybrid
    api_surface: "OData v4, RFC/BAPI, IDocs"
  - name: "Oracle NetSuite"
    vendor: "Oracle"
    version: "SuiteTalk 2024.1+"
    edition: "All editions"
    deployment: cloud
    api_surface: "REST, SuiteTalk SOAP, SuiteQL, RESTlets"
  - name: "Microsoft Dynamics 365 F&O"
    vendor: "Microsoft"
    version: "Dataverse 2024+"
    edition: "Enterprise"
    deployment: cloud
    api_surface: "OData v4, Web API, Data Management Framework"
  - name: "Salesforce"
    vendor: "Salesforce"
    version: "API v62.0+"
    edition: "Enterprise, Unlimited"
    deployment: cloud
    api_surface: "REST, SOAP, Bulk, Composite, Platform Events"
  - name: "Oracle ERP Cloud"
    vendor: "Oracle"
    version: "24B+"
    edition: "Enterprise"
    deployment: cloud
    api_surface: "REST, SOAP, FBDI, BICC, Business Events"

# === VERIFICATION ===
last_verified: 2026-03-07
confidence: 0.85
version: 1.0
first_published: 2026-03-07

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: evolving
  last_breaking_change: "Domain-driven canonical patterns (bounded context alignment) mainstream adoption 2024-2025"
  next_review: 2026-09-03
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "A canonical model is NOT a merge of all source system schemas — it is a new, independent model designed from business semantics"
  - "Without schema versioning (semver), a single breaking change to the canonical model breaks all connected systems simultaneously"
  - "Canonical models reduce N*(N-1)/2 point-to-point mappings to 2*N adapters — but only if governance is enforced"
  - "Over-engineering the canonical model upfront delays delivery — start with 6-12 core attributes per entity, extend additively"
  - "Each system adapter (to/from canonical) must be owned by the domain team, not a central integration team — otherwise the central team becomes a bottleneck"
  - "Forcing canonical adoption inside bounded contexts creates coupling — use Anti-Corruption Layers at domain boundaries instead"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User needs to integrate only 2 systems with stable schemas"
    use_instead: "Direct field mapping — canonical overhead not justified below 3 systems"
  - condition: "User needs master data management with match/merge/survivorship"
    use_instead: "business/erp-integration/master-data-management-erp/2026"
  - condition: "User needs data migration (one-time move, not ongoing integration)"
    use_instead: "ETL migration playbooks — canonical model is for ongoing integration"
  - condition: "User needs field-level mapping reference between two specific ERPs"
    use_instead: "business/erp-integration/field-mapping-data-type-comparison/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: system_count
    question: "How many systems need to exchange data?"
    type: choice
    options:
      - "2 systems (consider direct mapping instead)"
      - "3-5 systems (canonical model recommended)"
      - "6+ systems (canonical model essential)"
  - key: entity_scope
    question: "Which business entities need canonical representation?"
    type: choice
    options:
      - "Customer/Account only"
      - "Customer + Product + Order"
      - "Full suite (Customer, Product, Order, Invoice, Payment, Employee)"
  - key: change_frequency
    question: "How often do your source system schemas change?"
    type: choice
    options:
      - "Rarely (1-2 times per year)"
      - "Quarterly (ERP release cycles)"
      - "Frequently (monthly+ schema changes)"

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/business/erp-integration/canonical-data-model-design/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-03-07)"

# === RELATED UNITS ===
related_kos:
  depends_on:
    - id: "business/erp-integration/field-mapping-data-type-comparison/2026"
      label: "Field mapping and data type comparison across ERPs"
  related_to:
    - id: "business/erp-integration/master-data-management-erp/2026"
      label: "MDM golden record, survivorship rules, and sync patterns"
    - id: "business/erp-integration/change-data-capture-erp/2026"
      label: "Change Data Capture patterns for ERP event propagation"
    - id: "business/erp-integration/error-handling-dead-letter-queues/2026"
      label: "Error handling and dead letter queue patterns"
  solves:
    - id: "business/erp-integration/order-to-cash-integration/2026"
      label: "Order-to-cash cross-ERP integration playbook"
    - id: "business/erp-integration/procure-to-pay-integration/2026"
      label: "Procure-to-pay cross-ERP integration playbook"
  alternative_to:
    - id: "business/erp-integration/batch-vs-realtime-integration/2026"
      label: "Batch vs real-time integration pattern selection"
  often_confused_with:
    - id: "business/erp-integration/master-data-management-erp/2026"
      label: "MDM (match/merge/survivorship) vs CDM (schema standardization) — different problems"

# === SOURCES ===
sources:
  - id: src1
    title: "Canonical Data Model — Enterprise Integration Patterns"
    author: Gregor Hohpe, Bobby Woolf
    url: https://www.enterpriseintegrationpatterns.com/patterns/messaging/CanonicalDataModel.html
    type: technical_blog
    published: 2003-10-01
    reliability: authoritative
  - id: src2
    title: "What Is a Canonical Data Model? CDMs Explained"
    author: BMC Software
    url: https://www.bmc.com/blogs/canonical-data-model/
    type: technical_blog
    published: 2024-06-15
    reliability: high
  - id: src3
    title: "Canonical Data Models: A Comprehensive Guide"
    author: Alation
    url: https://www.alation.com/blog/canonical-data-models-explained-benefits-tools-getting-started/
    type: technical_blog
    published: 2025-01-20
    reliability: high
  - id: src4
    title: "Field Mapping vs. Canonical Data Model — Which One Wins in Integrations?"
    author: Artem Syryi
    url: https://dev.to/asiryi/field-mapping-vs-canonical-data-model-which-one-wins-in-integrations-2oj0
    type: community_resource
    published: 2025-03-10
    reliability: moderate_high
  - id: src5
    title: "Canonical Data Models: A Practical Guide"
    author: Beefed.ai
    url: https://beefed.ai/en/canonical-data-models-guide
    type: technical_blog
    published: 2025-06-01
    reliability: high
  - id: src6
    title: "Canonical Models in REST API Design"
    author: digitalML
    url: https://www.digitalml.com/canonical-models-should-be-a-core-component-of-your-api-strategy/
    type: technical_blog
    published: 2024-09-15
    reliability: moderate_high
  - id: src7
    title: "Best Practices for Evolving Schemas in Schema Registry"
    author: Solace
    url: https://docs.solace.com/Schema-Registry/schema-registry-best-practices.htm
    type: official_docs
    published: 2025-04-01
    reliability: high
---

# Canonical Data Model Design for Multi-ERP Integration

## TL;DR

- **Bottom line**: A canonical data model (CDM) replaces N*(N-1)/2 point-to-point translations with 2*N adapters by defining a single, ERP-independent schema that all systems map to/from — essential when integrating 3+ systems, overkill for 2.
- **Key limit**: Start with 6-12 core attributes per entity (Customer, Order, Product) — over-engineering the model upfront is the #1 cause of failed CDM initiatives.
- **Watch out for**: Basing your canonical model on one ERP's schema (typically SAP or Salesforce) — this creates hidden coupling and forces every other system into that vendor's data model.
- **Best for**: Multi-ERP environments with 3+ systems exchanging Customer, Product, Order, Invoice, or Payment data through middleware or event-driven architecture.
- **Versioning**: Use semantic versioning (major.minor.patch) with backward-compatible additive changes; breaking changes require a new major version with a deprecation window.

## System Profile

This is an architecture pattern card that applies across all major ERP systems. The canonical data model pattern is ERP-agnostic by design — it sits between systems as a neutral intermediary. The specific system adapters (transformers to/from canonical format) are system-specific, but the canonical schema itself is independent.

This card covers the design, versioning, and governance of canonical schemas for Customer, Product, Order, Invoice, Payment, and Employee entities across SAP S/4HANA, Oracle NetSuite, Dynamics 365, Salesforce, and Oracle ERP Cloud. It does NOT cover master data management (match/merge/survivorship), which is a separate discipline — see the MDM card.

| System | Role | API Surface | Direction |
|---|---|---|---|
| SAP S/4HANA | ERP — financial/logistics master | OData v4, RFC/BAPI | Bidirectional |
| Oracle NetSuite | ERP — mid-market financial/inventory | REST, SuiteTalk, SuiteQL | Bidirectional |
| Microsoft Dynamics 365 F&O | ERP — finance/supply chain | OData v4, Web API, DMF | Bidirectional |
| Salesforce | CRM — customer/opportunity master | REST v62.0, Composite, Platform Events | Bidirectional |
| Oracle ERP Cloud | ERP — enterprise financial/procurement | REST, FBDI, Business Events | Bidirectional |
| iPaaS / Middleware | Integration orchestrator | Varies | Orchestrator |

## API Surfaces & Capabilities

The canonical model is not an API surface itself — it is a schema contract that sits between system-specific adapters. Each system exposes its own API surfaces that the adapters consume.

| System | Primary API for Read | Primary API for Write | Event/CDC Support | Canonical Adapter Complexity |
|---|---|---|---|---|
| SAP S/4HANA | OData v4 (CDS views) | OData v4, BAPI | Business Events, AIF | High — deep customization, variant fields |
| NetSuite | SuiteQL, REST | SuiteTalk SOAP, REST | User Event Scripts, SuiteScript | Medium — flexible but governance-limited |
| Dynamics 365 | Web API (OData v4) | Web API, DMF | Dataverse webhooks, Dual Write | Medium — OData well-structured |
| Salesforce | REST API, SOQL | REST API, Composite | Platform Events, CDC | Medium — well-documented but governor limits |
| Oracle ERP Cloud | REST API | REST API, FBDI | Business Events, BICC | High — FBDI for bulk, REST for real-time |

## Rate Limits & Quotas

Rate limits apply per-system at the adapter layer, not at the canonical model layer. Canonical model design must account for the slowest/most-restrictive system in the chain.

### Adapter Throughput Planning

| System | Effective Read Rate | Effective Write Rate | Bulk Import | Bottleneck |
|---|---|---|---|---|
| SAP S/4HANA | ~5,000 records/min (OData paging) | ~1,000 records/min (BAPI) | IDocs (batch), BAPI mass | Custom code governor limits |
| NetSuite | ~2,000 records/min (SuiteQL) | ~500 records/min (SuiteTalk) | CSV import (500K records) | Governance units (10,000/script) |
| Dynamics 365 | ~10,000 records/min (Web API) | ~5,000 records/min (Web API) | DMF (millions) | Throttling at 6,000 req/5min |
| Salesforce | ~2,000 records/req (SOQL) | 200 records/req (Composite) | Bulk API 2.0 (150MB/file) | 100K API calls/24h (Enterprise) |
| Oracle ERP Cloud | ~5,000 records/min (REST) | ~1,000 records/min (REST) | FBDI (250MB/file) | Fair-use throttling |

[src1, src2]

### Design Implication

When designing canonical transformations, batch size must match the most constrained system. If Salesforce limits you to 200 records per Composite request, your canonical batch processor should chunk at 200 even if SAP can handle 5,000 per call. [src2]

## Authentication

Authentication is handled per-system adapter, not at the canonical model layer. Each adapter authenticates independently with its target system.

| System | Recommended Auth | Token Lifetime | Notes |
|---|---|---|---|
| SAP S/4HANA | OAuth 2.0 (SAP BTP) or X.509 | Session-based | Principal propagation for user-context |
| NetSuite | Token-Based Authentication (TBA) | Persistent tokens | OAuth 2.0 available but TBA simpler for server-to-server |
| Dynamics 365 | OAuth 2.0 Client Credentials | 1h access token | Azure AD app registration required |
| Salesforce | OAuth 2.0 JWT Bearer | 2h session | Connected app with digital certificate |
| Oracle ERP Cloud | OAuth 2.0 or Basic Auth | Token-based | Basic Auth still common for FBDI/ESS jobs |

### Authentication Gotchas

- Canonical model transformations running as scheduled jobs must handle token refresh for ALL connected systems — if one token expires mid-batch, the entire canonical sync fails [src2]
- SAP OAuth tokens via BTP have different scopes than direct S/4HANA tokens — ensure the adapter's service account has the correct authorization objects [src5]
- Salesforce JWT bearer flow tokens are org-scoped — the integration user's profile determines which fields the adapter can read/write, potentially causing silent canonical mapping gaps [src2]

## Constraints
<!-- Agents: read this section before recommending any integration approach from this unit.
     These are hard constraints that must not be violated. -->

- **Not a merge of source schemas**: A canonical model is a NEW model designed from business semantics, not a union of all fields from all systems. Merging schemas produces an unmaintainable "god model." [src2]
- **Minimum 3 systems to justify**: For 2 systems, direct field mapping has lower overhead and equivalent maintainability. The CDM overhead pays off at 3+ systems where point-to-point mappings grow quadratically. [src1, src4]
- **Schema versioning is mandatory**: Without semantic versioning, a single field rename or type change breaks every adapter simultaneously. Use BACKWARD compatibility mode as default. [src5, src7]
- **Additive-only evolution**: Normal changes must add optional fields. Removing required fields, changing field types, or renaming fields without aliases are breaking changes requiring a new major version. [src7]
- **Domain ownership of adapters**: Each system's adapter must be owned by that system's domain team. A central integration team owning all adapters becomes a bottleneck and single point of failure. [src5]
- **Anti-Corruption Layer at boundaries**: Do not force canonical formats inside bounded contexts. Use ACL adapters at domain boundaries to translate to/from canonical form. [src5]

## Integration Pattern Decision Tree

```
START — User needs to share data across 3+ ERP systems
├── How many systems exchange data?
│   ├── 2 systems only
│   │   └── Direct field mapping — CDM overhead not justified
│   │       (But plan for CDM if a 3rd system is expected within 12 months)
│   ├── 3-5 systems
│   │   └── Canonical Data Model recommended
│   │       ├── Start with core entities (Customer, Order, Product)
│   │       ├── 6-12 attributes per entity initially
│   │       └── Add entities/attributes incrementally per use case
│   └── 6+ systems
│       └── Canonical Data Model essential
│           ├── Invest in schema registry (Confluent, Apicurio, AWS Glue)
│           ├── Formal governance with domain stewards
│           └── CI/CD compatibility checks on every schema change
├── What approach to canonical ownership?
│   ├── Centralized (one team owns canonical + all adapters)
│   │   └── Anti-pattern for >3 systems — bottleneck risk
│   ├── Federated (domain teams own adapters, governance board owns canonical)
│   │   └── Recommended — scales with organization
│   └── Hybrid (central canonical, domain adapters)
│       └── Good starting point — evolve to federated
├── What schema format?
│   ├── JSON Schema — REST APIs, event payloads, iPaaS
│   ├── Avro — Kafka event streaming, schema registry native
│   ├── Protobuf — gRPC, high-performance serialization
│   └── XML Schema (XSD) — SOAP, EDI, legacy middleware
└── What evolution strategy?
    ├── Additive only (minor versions) — default for all changes
    ├── Breaking change (major version) — deprecation window + migration plan
    └── Transform-on-read — store in canonical, transform at consumption time
```

## Quick Reference

### Canonical Entity to System-Specific Mapping

| Canonical Entity | SAP S/4HANA | NetSuite | Dynamics 365 | Salesforce | Oracle ERP Cloud |
|---|---|---|---|---|---|
| **Customer** | BusinessPartner (BP) | customer (entity) | Account (Dataverse) | Account (sObject) | hz_parties / hz_cust_accounts |
| **Product** | Product (A_Product) | item (entity) | Product (Released) | Product2 (sObject) | egp_system_items |
| **Order** | SalesOrder (A_SalesOrder) | salesOrder (entity) | SalesOrderHeader | Opportunity / Order | doo_order_headers |
| **Invoice** | BillingDocument | invoice (entity) | CustInvoiceJour | Invoice (custom) | ra_customer_trx |
| **Payment** | FI Document (BKPF/BSEG) | customerPayment (entity) | CustPaymJournalTrans | Payment (custom) | ap_checks / ar_cash_receipts |
| **Employee** | Employee (A_Employee) | employee (entity) | HcmWorker | Contact / User | per_all_people_f |
| **Vendor/Supplier** | Supplier (A_Supplier) | vendor (entity) | VendTable | Account (Supplier type) | poz_suppliers |
| **Address** | A_BPAddress | address (subrecord) | LogisticsPostalAddress | Address (compound) | hz_locations |

[src1, src2, src3]

### Canonical Customer Schema (JSON Schema)

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://acme.example/schemas/canonical/Customer.v1.json",
  "title": "Canonical Customer",
  "description": "Canonical Customer entity for cross-ERP integration. Version 1.x — additive changes only.",
  "type": "object",
  "properties": {
    "canonicalId": {
      "type": "string",
      "pattern": "^urn:acme:customer:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
      "description": "Immutable canonical identifier — URN format with UUID"
    },
    "legalName": {
      "type": "string",
      "maxLength": 255,
      "description": "Official legal name — source of truth varies by system"
    },
    "tradingName": {
      "type": ["string", "null"],
      "maxLength": 255,
      "description": "DBA or trading name — optional, not all ERPs distinguish this"
    },
    "customerType": {
      "type": "string",
      "enum": ["individual", "organization", "government"],
      "description": "Canonical customer classification"
    },
    "primaryEmail": {
      "type": ["string", "null"],
      "format": "email"
    },
    "primaryPhone": {
      "type": ["string", "null"],
      "pattern": "^\\+[1-9]\\d{1,14}$",
      "description": "E.164 format — adapters must normalize from system-specific formats"
    },
    "taxId": {
      "type": ["string", "null"],
      "description": "Tax identification number — format varies by jurisdiction"
    },
    "currency": {
      "type": "string",
      "pattern": "^[A-Z]{3}$",
      "description": "ISO 4217 currency code — canonical default currency"
    },
    "status": {
      "type": "string",
      "enum": ["active", "inactive", "prospect", "blocked"],
      "description": "Canonical lifecycle status — mapped from system-specific statuses"
    },
    "addresses": {
      "type": "array",
      "items": { "$ref": "#/$defs/Address" }
    },
    "systemReferences": {
      "type": "array",
      "items": { "$ref": "#/$defs/SystemReference" },
      "description": "Cross-reference IDs in each connected system"
    },
    "createdAt": {
      "type": "string",
      "format": "date-time",
      "description": "UTC timestamp — all canonical timestamps are UTC"
    },
    "updatedAt": {
      "type": "string",
      "format": "date-time"
    },
    "schemaVersion": {
      "type": "string",
      "pattern": "^\\d+\\.\\d+\\.\\d+$",
      "description": "Semantic version of the canonical schema this record conforms to"
    }
  },
  "required": ["canonicalId", "legalName", "customerType", "status", "createdAt", "schemaVersion"],
  "$defs": {
    "Address": {
      "type": "object",
      "properties": {
        "addressType": { "type": "string", "enum": ["billing", "shipping", "legal", "other"] },
        "line1": { "type": "string", "maxLength": 255 },
        "line2": { "type": ["string", "null"], "maxLength": 255 },
        "city": { "type": "string", "maxLength": 100 },
        "stateProvince": { "type": ["string", "null"], "maxLength": 100 },
        "postalCode": { "type": ["string", "null"], "maxLength": 20 },
        "country": { "type": "string", "pattern": "^[A-Z]{2}$", "description": "ISO 3166-1 alpha-2" },
        "isPrimary": { "type": "boolean", "default": false }
      },
      "required": ["addressType", "line1", "city", "country"]
    },
    "SystemReference": {
      "type": "object",
      "properties": {
        "system": { "type": "string", "description": "Source system identifier — e.g., 'sap-s4', 'netsuite', 'sfdc'" },
        "externalId": { "type": "string", "description": "ID in the source system" },
        "entityType": { "type": "string", "description": "Source entity name — e.g., 'BusinessPartner', 'Account'" },
        "lastSyncedAt": { "type": "string", "format": "date-time" }
      },
      "required": ["system", "externalId"]
    }
  }
}
```

[src3, src5]

### Canonical Order Schema (JSON Schema)

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://acme.example/schemas/canonical/Order.v1.json",
  "title": "Canonical Order",
  "description": "Canonical Order entity for cross-ERP integration.",
  "type": "object",
  "properties": {
    "canonicalId": {
      "type": "string",
      "pattern": "^urn:acme:order:[0-9a-f\\-]{36}$"
    },
    "customerRef": {
      "type": "string",
      "description": "Canonical customer ID (URN) — resolved via systemReferences"
    },
    "orderType": {
      "type": "string",
      "enum": ["sales", "purchase", "transfer", "return"]
    },
    "orderDate": {
      "type": "string",
      "format": "date",
      "description": "Business date of order — not submission timestamp"
    },
    "currency": {
      "type": "string",
      "pattern": "^[A-Z]{3}$"
    },
    "totalAmount": {
      "type": "number",
      "description": "Decimal — always in the currency specified. Adapters handle unit conversion."
    },
    "taxAmount": {
      "type": ["number", "null"]
    },
    "status": {
      "type": "string",
      "enum": ["draft", "confirmed", "processing", "shipped", "delivered", "cancelled", "returned"]
    },
    "lineItems": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "lineNumber": { "type": "integer" },
          "productRef": { "type": "string", "description": "Canonical product ID (URN)" },
          "quantity": { "type": "number" },
          "unitPrice": { "type": "number" },
          "uom": { "type": "string", "description": "ISO 20765 or UN/CEFACT unit of measure" },
          "lineTotal": { "type": "number" }
        },
        "required": ["lineNumber", "productRef", "quantity", "unitPrice"]
      }
    },
    "systemReferences": {
      "type": "array",
      "items": { "type": "object" }
    },
    "createdAt": { "type": "string", "format": "date-time" },
    "updatedAt": { "type": "string", "format": "date-time" },
    "schemaVersion": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" }
  },
  "required": ["canonicalId", "customerRef", "orderType", "orderDate", "currency", "status", "lineItems", "createdAt", "schemaVersion"]
}
```

[src3, src5]

## Step-by-Step Integration Guide

### 1. Inventory systems and identify core entities

Map every system in your integration landscape and identify which business entities flow between them. Start with the highest-volume, highest-value entities. [src3]

```
Priority Matrix:
| Entity    | Systems Sharing It | Volume/Day | Priority |
|-----------|-------------------|------------|----------|
| Customer  | 5 (all ERPs + CRM)| 500 creates| P0       |
| Order     | 4 (CRM + 3 ERPs) | 2,000      | P0       |
| Product   | 4 (PIM + 3 ERPs)  | 50 updates | P1       |
| Invoice   | 3 (ERP + billing) | 5,000      | P1       |
| Payment   | 2 (ERP + bank)    | 1,000      | P2       |
```

**Verify**: Each entity appears in 3+ systems (otherwise use direct mapping for that entity).

### 2. Design the canonical schema per entity

For each P0 entity, define the canonical schema with 6-12 core attributes. Use JSON Schema (for REST/event payloads) or Avro (for Kafka streaming). [src3, src5]

```bash
# Validate canonical schema using ajv-cli
npm install -g ajv-cli
ajv validate -s canonical/Customer.v1.json -d sample-customer.json
# Expected: valid
```

**Verify**: Schema validates against sample payloads from each source system.

### 3. Build system-specific adapters (Anti-Corruption Layers)

Each adapter translates between one system's native format and the canonical format. Adapters are owned by domain teams. [src5]

```python
# adapter_salesforce_customer.py
# Input:  Salesforce Account sObject (JSON from REST API)
# Output: Canonical Customer (JSON matching Customer.v1.json)

def salesforce_to_canonical(sf_account: dict) -> dict:
    """Transform Salesforce Account to Canonical Customer."""
    return {
        "canonicalId": resolve_canonical_id("sfdc", sf_account["Id"]),
        "legalName": sf_account["Name"],
        "tradingName": sf_account.get("DBA_Name__c"),
        "customerType": map_sf_type(sf_account.get("Type", "Prospect")),
        "primaryEmail": sf_account.get("PersonEmail"),
        "primaryPhone": normalize_e164(sf_account.get("Phone")),
        "taxId": sf_account.get("Tax_ID__c"),
        "currency": sf_account.get("CurrencyIsoCode", "USD"),
        "status": map_sf_status(sf_account.get("Account_Status__c", "Active")),
        "addresses": build_addresses(sf_account),
        "systemReferences": [{
            "system": "sfdc",
            "externalId": sf_account["Id"],
            "entityType": "Account",
            "lastSyncedAt": datetime.utcnow().isoformat() + "Z"
        }],
        "createdAt": sf_account["CreatedDate"],
        "updatedAt": sf_account["LastModifiedDate"],
        "schemaVersion": "1.0.0"
    }

def map_sf_type(sf_type: str) -> str:
    """Map Salesforce Account Type to canonical customerType."""
    mapping = {
        "Prospect": "prospect",
        "Customer - Direct": "organization",
        "Customer - Channel": "organization",
        "Technology Partner": "organization",
        "Government": "government",
    }
    return mapping.get(sf_type, "organization")

def map_sf_status(sf_status: str) -> str:
    """Map Salesforce status to canonical status."""
    mapping = {
        "Active": "active",
        "Inactive": "inactive",
        "Prospect": "prospect",
        "Credit Hold": "blocked",
    }
    return mapping.get(sf_status, "active")
```

**Verify**: Run adapter against 100 real Salesforce records, validate output against canonical schema.

### 4. Register schemas and enforce compatibility

Use a schema registry to store canonical schemas with version history and compatibility enforcement. [src7]

```bash
# Register canonical Customer schema in Confluent Schema Registry
curl -X POST http://schema-registry:8081/subjects/canonical-customer-value/versions \
  -H "Content-Type: application/vnd.schemaregistry.v1+json" \
  -d '{
    "schemaType": "JSON",
    "schema": "{... Customer.v1.json content ...}"
  }'
# Expected: {"id": 1}

# Set compatibility mode to BACKWARD (default — new schemas can read old data)
curl -X PUT http://schema-registry:8081/config/canonical-customer-value \
  -H "Content-Type: application/vnd.schemaregistry.v1+json" \
  -d '{"compatibility": "BACKWARD"}'
# Expected: {"compatibility":"BACKWARD"}
```

**Verify**: Attempt to register an incompatible schema change (e.g., remove required field) — registry should reject it.

### 5. Implement canonical transformation pipeline

Wire adapters into your integration platform (iPaaS, Kafka, or custom middleware). [src1, src5]

```python
# canonical_pipeline.py
# Input:  Events from source systems (Salesforce, SAP, NetSuite)
# Output: Canonical events published to canonical topic

import json
from jsonschema import validate, ValidationError

# Load canonical schema once at startup
with open("schemas/canonical/Customer.v1.json") as f:
    CUSTOMER_SCHEMA = json.load(f)

ADAPTERS = {
    "sfdc": salesforce_to_canonical,
    "sap-s4": sap_to_canonical,
    "netsuite": netsuite_to_canonical,
    "d365": dynamics_to_canonical,
}

def process_event(source_system: str, payload: dict) -> dict:
    """Transform source event to canonical format with validation."""
    adapter = ADAPTERS.get(source_system)
    if not adapter:
        raise ValueError(f"No adapter registered for {source_system}")

    canonical = adapter(payload)

    # Validate against canonical schema — fail fast on bad data
    try:
        validate(instance=canonical, schema=CUSTOMER_SCHEMA)
    except ValidationError as e:
        log_to_dead_letter_queue(source_system, payload, str(e))
        raise

    return canonical
```

**Verify**: Process a test event from each source system, confirm canonical output validates against schema.

### 6. Implement schema evolution workflow

When business requirements demand new fields, follow the additive evolution process. [src7]

```bash
# Example: Adding "industryCode" (optional field) to Customer v1.1.0
# Step 1: Update schema — add new optional field
# Step 2: Run compatibility check
curl -X POST http://schema-registry:8081/compatibility/subjects/canonical-customer-value/versions/latest \
  -H "Content-Type: application/vnd.schemaregistry.v1+json" \
  -d '{"schemaType": "JSON", "schema": "{... Customer.v1.1.json ...}"}'
# Expected: {"is_compatible": true}

# Step 3: Register new version
curl -X POST http://schema-registry:8081/subjects/canonical-customer-value/versions \
  -H "Content-Type: application/vnd.schemaregistry.v1+json" \
  -d '{"schemaType": "JSON", "schema": "{... Customer.v1.1.json ...}"}'
# Expected: {"id": 2}

# Step 4: Update adapters incrementally — old adapters continue working
# because new field is optional (BACKWARD compatible)
```

**Verify**: Old adapter output (without new field) still validates against v1.1.0 schema.

## Code Examples

### Python: Bidirectional adapter with canonical validation

```python
# Input:  Source system payload (any connected ERP)
# Output: Validated canonical Customer or validated system-specific payload

import json
from jsonschema import validate, ValidationError
from datetime import datetime, timezone

class CanonicalAdapter:
    """Bidirectional adapter between canonical and system-specific formats."""

    def __init__(self, schema_path: str, system_name: str):
        with open(schema_path) as f:
            self.schema = json.load(f)
        self.system = system_name

    def to_canonical(self, source_payload: dict) -> dict:
        """Transform system-specific payload to canonical format."""
        raise NotImplementedError("Subclass must implement to_canonical")

    def from_canonical(self, canonical_payload: dict) -> dict:
        """Transform canonical payload to system-specific format."""
        raise NotImplementedError("Subclass must implement from_canonical")

    def validate_canonical(self, canonical: dict) -> bool:
        """Validate payload against canonical JSON Schema."""
        try:
            validate(instance=canonical, schema=self.schema)
            return True
        except ValidationError as e:
            raise ValueError(
                f"Canonical validation failed for {self.system}: "
                f"{e.message} at path {list(e.absolute_path)}"
            )

class SAPCustomerAdapter(CanonicalAdapter):
    """SAP S/4HANA BusinessPartner <-> Canonical Customer."""

    SAP_STATUS_MAP = {
        "1": "active",     # Active
        "2": "blocked",    # Blocked
        "3": "inactive",   # Marked for deletion
    }

    def to_canonical(self, bp: dict) -> dict:
        canonical = {
            "canonicalId": self._resolve_id(bp["BusinessPartner"]),
            "legalName": bp["BusinessPartnerFullName"],
            "tradingName": bp.get("SearchTerm1"),
            "customerType": "organization" if bp.get("BusinessPartnerCategory") == "2" else "individual",
            "primaryEmail": bp.get("EmailAddress"),
            "primaryPhone": self._normalize_phone(bp.get("PhoneNumber")),
            "currency": bp.get("Currency", "EUR"),
            "status": self.SAP_STATUS_MAP.get(bp.get("AuthorizationGroup", "1"), "active"),
            "systemReferences": [{
                "system": "sap-s4",
                "externalId": bp["BusinessPartner"],
                "entityType": "A_BusinessPartner",
                "lastSyncedAt": datetime.now(timezone.utc).isoformat()
            }],
            "createdAt": bp.get("CreationDate", datetime.now(timezone.utc).isoformat()),
            "updatedAt": bp.get("LastChangeDate", datetime.now(timezone.utc).isoformat()),
            "schemaVersion": "1.0.0"
        }
        self.validate_canonical(canonical)
        return canonical
```

### JavaScript/Node.js: Schema version migration utility

```javascript
// Input:  Canonical payload in schema v1.0.0
// Output: Canonical payload migrated to schema v1.1.0

const Ajv = require("ajv/dist/2020");  // ajv v8.x
const addFormats = require("ajv-formats");

const ajv = new Ajv({ allErrors: true });
addFormats(ajv);

// Register all schema versions
const schemas = {
  "1.0.0": require("./schemas/Customer.v1.0.0.json"),
  "1.1.0": require("./schemas/Customer.v1.1.0.json"),
};

// Migration functions — one per version transition
const migrations = {
  "1.0.0->1.1.0": (payload) => ({
    ...payload,
    industryCode: null,  // New optional field — default null
    schemaVersion: "1.1.0",
  }),
};

function migratePayload(payload) {
  const currentVersion = payload.schemaVersion || "1.0.0";
  const targetVersion = "1.1.0";

  if (currentVersion === targetVersion) return payload;

  const migrationKey = `${currentVersion}->${targetVersion}`;
  const migrateFn = migrations[migrationKey];
  if (!migrateFn) {
    throw new Error(`No migration path from ${currentVersion} to ${targetVersion}`);
  }

  const migrated = migrateFn(payload);

  // Validate migrated payload against target schema
  const validate = ajv.compile(schemas[targetVersion]);
  if (!validate(migrated)) {
    throw new Error(`Migration validation failed: ${JSON.stringify(validate.errors)}`);
  }

  return migrated;
}

module.exports = { migratePayload };
```

## Data Mapping

### Field Mapping Reference — Canonical Customer to System-Specific

| Canonical Field | SAP S/4HANA | NetSuite | Dynamics 365 | Salesforce | Gotcha |
|---|---|---|---|---|---|
| canonicalId | Custom: Z_CANONICAL_ID | Custom: custentity_canonical_id | Custom: cr_canonicalid | Custom: Canonical_ID__c | Must be indexed in every system for reverse lookup |
| legalName | BusinessPartnerFullName | companyName | Name (Account) | Name | SAP max 81 chars, NetSuite max 83, SF max 255 |
| customerType | BusinessPartnerCategory (1=Person, 2=Org) | isPerson (boolean) | RelationshipType | RecordTypeId | SAP uses codes, NetSuite uses boolean, SF uses record types |
| primaryEmail | EmailAddress (BP contact) | email | EMailAddress | PersonEmail or Email__c | SAP email lives on contact, not BP directly |
| primaryPhone | PhoneNumber (intl format varies) | phone | Telephone | Phone | Normalize to E.164 in all adapters |
| currency | Currency (T001-WAERS) | currency.refName | TransactionCurrencyId | CurrencyIsoCode | SAP stores as company code default, others per-record |
| status | AuthorizationGroup | entityStatus.refName | StateCode (0=Active, 1=Inactive) | Account_Status__c (custom) | Each system has different lifecycle states — map carefully |
| taxId | TaxNumber1 | defaultTaxReg | VATNum | Tax_ID__c (custom) | Format varies by jurisdiction — validate per country |
| addresses | A_BPAddress (1:N relationship) | addressbook (sublist) | LogisticsPostalAddress | BillingAddress, ShippingAddress | SAP has complex address time-dependency; SF has only 2 address types by default |

[src2, src4, src5]

### Data Type Gotchas

- **Datetime timezone mismatch**: SAP stores dates in user timezone, Salesforce always UTC, NetSuite in company timezone, Dynamics 365 in UTC. All canonical timestamps MUST be UTC — adapters handle conversion. [src2]
- **Amount/currency representation**: SAP stores amounts in smallest currency unit (cents/pfennig) for some currencies; Salesforce and NetSuite store as decimal. Canonical model should use decimal with explicit currency code. [src5]
- **Boolean representation**: SAP uses "X" (true) / "" (false), NetSuite uses true/false, Dynamics 365 uses 0/1, Salesforce uses true/false. Normalize to JSON boolean in canonical. [src4]
- **Multi-value fields**: Salesforce multi-select picklists use semicolons ("A;B;C"), SAP uses separate table entries, NetSuite uses arrays. Canonical model should use JSON arrays. [src2]
- **ID formats**: SAP Business Partner is 10-digit zero-padded string, Salesforce is 18-char alphanumeric, NetSuite is integer, Dynamics 365 is GUID. Canonical uses URN with UUID. [src5]

## Error Handling & Failure Points

### Common Error Codes

| Error | System | Meaning | Resolution |
|---|---|---|---|
| VALIDATION_FAILED | Canonical | Payload doesn't conform to canonical JSON Schema | Check adapter transformation logic — usually a missing required field or wrong type |
| SCHEMA_VERSION_MISMATCH | Canonical | Payload schemaVersion doesn't match expected | Run migration utility to upgrade payload to current schema version |
| DUPLICATE_CANONICAL_ID | Canonical | Same canonicalId already exists with different systemReferences | Likely a merge scenario — route to MDM for deduplication |
| ADAPTER_NOT_FOUND | Pipeline | No adapter registered for source system | Register adapter before connecting new system |
| INCOMPATIBLE_SCHEMA | Schema Registry | Schema change breaks backward compatibility | Revert change; create new major version if breaking change is required |
| TRANSFORM_TIMEOUT | Adapter | Adapter took >30s to transform | Check source system API response time — may need to increase timeout or batch smaller |
| DEAD_LETTER_QUEUE_FULL | Pipeline | DLQ capacity exceeded | Alert ops team; investigate root cause of high failure rate |

[src1, src5]

### Failure Points in Production

- **Schema drift between environments**: Dev/staging canonical schemas diverge from production after a partial deployment. Fix: `CI/CD pipeline that validates schema compatibility before deploying any adapter — block deployment if schemas don't match.` [src7]
- **Adapter version skew**: Adapter A produces v1.1.0 canonical payloads but adapter B only understands v1.0.0. Fix: `BACKWARD compatibility mode in schema registry ensures old consumers can always read new data — enforce as default.` [src7]
- **Canonical ID resolution failure**: System reference lookup fails when a record exists in the source system but hasn't been synced to the canonical ID registry yet. Fix: `Implement provisional ID assignment with a reconciliation job that resolves provisional IDs to canonical IDs within 15 minutes.` [src5]
- **Silent data loss on optional fields**: Adapter maps a required source field to an optional canonical field, then the target adapter ignores null optional fields. Fix: `Document field cardinality matrix — track which fields are required in each source system vs optional in canonical.` [src3]
- **Timezone corruption on date fields**: Adapter doesn't convert timezone before writing to canonical — dates shift by hours. Fix: `Enforce UTC-only policy in canonical schema; add timezone validation in adapter unit tests.` [src2]

## Anti-Patterns

### Wrong: Basing canonical model on one ERP's schema

```python
# BAD — Using Salesforce Account fields as the canonical model
canonical_customer = {
    "Id": sf_account["Id"],                    # SF-specific 18-char ID
    "Name": sf_account["Name"],                # SF field name
    "BillingStreet": sf_account["BillingStreet"],  # SF compound address
    "Type": sf_account["Type"],                # SF picklist values
    "OwnerId": sf_account["OwnerId"],          # SF-specific concept
    "RecordTypeId": sf_account["RecordTypeId"], # SF-specific concept
}
# Result: Every other system must translate TO Salesforce's model.
# When Salesforce changes a field, ALL adapters break.
```

### Correct: Designing a neutral canonical model from business semantics

```python
# GOOD — Business-driven canonical model independent of any system
canonical_customer = {
    "canonicalId": "urn:acme:customer:550e8400-e29b-41d4-a716-446655440000",
    "legalName": "Acme Corporation",
    "customerType": "organization",
    "primaryEmail": "contact@acme.com",
    "status": "active",
    "systemReferences": [
        {"system": "sfdc", "externalId": "001xx000003DGbzAAG"},
        {"system": "sap-s4", "externalId": "0001000042"},
        {"system": "netsuite", "externalId": "12345"},
    ],
    "schemaVersion": "1.0.0"
}
# Result: Each system maps to/from a neutral model.
# Changing one system only affects that system's adapter.
```

### Wrong: Trying to model everything upfront

```python
# BAD — 200+ fields in the canonical Customer schema from day one
canonical_customer_schema = {
    "properties": {
        "customerId": {},
        "legalName": {},
        "tradingName": {},
        "dbaName": {},
        "parentCompany": {},
        "ultimateParent": {},
        "dunsNumber": {},
        "sic_code": {},
        "naics_code": {},
        "tickerSymbol": {},
        "exchange": {},
        "employeeCount": {},
        "annualRevenue": {},
        "revenueRange": {},
        # ... 180 more fields ...
        "customField47": {},
    }
}
# Result: 6 months of design meetings, no integration delivered.
# 80% of fields are empty in most systems. Schema changes are contentious.
```

### Correct: Start with core attributes, extend additively

```python
# GOOD — 12 core fields, extend as use cases demand
canonical_customer_v1 = {
    "properties": {
        "canonicalId": {},      # Immutable identifier
        "legalName": {},        # Required — every system has this
        "customerType": {},     # Required — individual vs organization
        "primaryEmail": {},     # Optional — not all ERPs have email
        "primaryPhone": {},     # Optional
        "currency": {},         # Required — financial operations need this
        "status": {},           # Required — lifecycle management
        "addresses": {},        # Array — flexible structure
        "systemReferences": {}, # Array — cross-reference IDs
        "createdAt": {},        # Required — audit trail
        "updatedAt": {},        # Required — change detection
        "schemaVersion": {},    # Required — evolution tracking
    }
}
# v1.1 adds: industryCode, taxId (both optional, backward compatible)
# v1.2 adds: parentCustomerRef (optional, backward compatible)
# v2.0 (breaking): changes addressType enum — requires migration plan
```

### Wrong: Central team owns all adapters

```
# BAD — Integration Center of Excellence owns everything
Team Structure:
  Integration CoE (5 people) owns:
    - Canonical schema (all entities)
    - Salesforce adapter
    - SAP adapter
    - NetSuite adapter
    - D365 adapter
    - Pipeline orchestration
    - Schema registry
    - Monitoring

Result: 5 people become bottleneck for all integration work.
Average lead time for new field: 6 weeks (backlog queue).
When someone leaves, institutional knowledge disappears.
```

### Correct: Federated ownership with governance board

```
# GOOD — Domain teams own adapters, governance board owns canonical
Team Structure:
  Canonical Governance Board (architects from each domain):
    - Reviews/approves canonical schema changes
    - Meets weekly, 30-min standing agenda
    - Owns schema registry and compatibility policy

  CRM Domain Team owns:
    - Salesforce adapter (to/from canonical)
    - Salesforce-specific field mapping
    - Salesforce Platform Event triggers

  Finance Domain Team owns:
    - SAP adapter (to/from canonical)
    - NetSuite adapter (to/from canonical)
    - Financial entity expertise

  Platform Team owns:
    - Schema registry infrastructure
    - CI/CD pipeline for adapter deployment
    - Monitoring and alerting

Result: New field request → domain team implements in their adapter
(1-2 days), governance board reviews canonical change (weekly).
Average lead time for new field: 1-2 weeks.
```

## Common Pitfalls

- **Over-engineering the initial model**: Teams spend 6+ months designing a "complete" canonical model before building any adapters. Fix: `Start with 3 entities (Customer, Order, Product), 6-12 fields each. Ship an adapter in 2-4 weeks. Add fields as use cases demand.` [src3]
- **No schema versioning from day one**: First schema change breaks all consumers because there's no version tracking. Fix: `Include schemaVersion as a required field in every canonical entity from the start. Register schemas in a registry with BACKWARD compatibility enforced.` [src7]
- **Testing adapters against mock data only**: Adapters work perfectly with clean test data but fail on real production data with nulls, encoding issues, and edge cases. Fix: `Test adapters against anonymized production data extracts. Build a canonical validation report that runs nightly against live data.` [src5]
- **Ignoring the cross-reference ID registry**: Systems can't find canonical IDs for records because there's no reliable cross-reference index. Fix: `Implement a dedicated cross-reference table (canonical_id, system, external_id) with unique constraints. Populate it before any data flows.` [src2]
- **Schema evolution without deprecation windows**: Breaking changes deployed instantly, breaking all consumers. Fix: `Major version changes require: (1) 30-day deprecation notice, (2) migration guide, (3) dual-version support for 90 days, (4) consumer migration verification before old version retirement.` [src7]
- **Treating canonical model as an MDM hub**: Teams expect the canonical model to deduplicate records, manage golden records, and resolve conflicts. Fix: `Canonical model is a schema contract, not a data store. For deduplication and golden records, use an MDM platform. CDM feeds INTO MDM, not replaces it.` [src2]

## Diagnostic Commands

```bash
# Validate a canonical payload against the schema
ajv validate -s schemas/canonical/Customer.v1.json -d payload.json --all-errors
# Expected: payload.json valid (or list of validation errors)

# Check schema registry for registered versions
curl -s http://schema-registry:8081/subjects/canonical-customer-value/versions | jq .
# Expected: [1, 2, 3] (list of registered version IDs)

# Check compatibility of a new schema version
curl -s -X POST http://schema-registry:8081/compatibility/subjects/canonical-customer-value/versions/latest \
  -H "Content-Type: application/vnd.schemaregistry.v1+json" \
  -d '{"schemaType":"JSON","schema":"..."}' | jq .
# Expected: {"is_compatible": true}

# List all system references for a canonical ID
SELECT system, external_id, last_synced_at
FROM canonical_cross_references
WHERE canonical_id = 'urn:acme:customer:550e8400-e29b-41d4-a716-446655440000';
# Expected: rows for each connected system (sfdc, sap-s4, netsuite, etc.)

# Count canonical validation failures in the last 24h
SELECT source_system, COUNT(*) as failures,
       array_agg(DISTINCT error_path) as failing_fields
FROM canonical_validation_log
WHERE created_at > NOW() - INTERVAL '24 hours'
GROUP BY source_system ORDER BY failures DESC;
# Expected: per-system failure counts and which fields are causing issues

# Check adapter version alignment
curl -s http://adapter-registry/api/v1/adapters | jq '.[] | {system, adapter_version, canonical_schema_version}'
# Expected: all adapters targeting same or compatible canonical schema version
```

## Version History & Compatibility

The canonical data model pattern itself is well-established (first documented by Hohpe & Woolf in 2003). What evolves is best practices for implementation.

| Era | Approach | Schema Format | Governance | Status |
|---|---|---|---|---|
| 2003-2015 | Enterprise-wide XML canonical model | XML Schema (XSD) | Central governance committee | Legacy — still in use at large enterprises |
| 2015-2020 | API-first canonical (REST/JSON) | JSON Schema, OpenAPI | API management platform | Mature — widely adopted |
| 2020-2024 | Event-driven canonical (streaming) | Avro, Protobuf, JSON Schema | Schema registry (Confluent, AWS Glue) | Current — recommended for new projects |
| 2024-2026 | Domain-driven canonical (bounded contexts) | JSON Schema + AsyncAPI | Federated governance with CI/CD gates | Current — emerging best practice |

[src1, src5, src7]

### Deprecation Policy

Schema versions follow semantic versioning. Minor versions (additive changes) are perpetually backward compatible. Major versions receive a minimum 90-day deprecation window with dual-version support. Organizations should plan for at most 2 active major versions at any time. [src7]

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Integrating 3+ systems that share Customer, Order, or Product data | Only 2 systems with stable, well-understood schemas | Direct field mapping with transformation layer |
| Systems are frequently replaced or upgraded (M&A, cloud migration) | All systems are from the same vendor (e.g., all-SAP landscape) | Vendor-native integration (SAP CPI, Oracle SOA) |
| Multiple teams need to build integrations independently | Single integration team handles all data flows | Point-to-point with shared mapping documentation |
| Event-driven architecture with Kafka/message bus | Simple batch ETL running once nightly | ETL/ELT with source-to-target mapping |
| Long-term strategic integration platform (3+ year horizon) | Short-term project or POC (< 6 months) | Direct mapping — refactor to CDM later if needed |
| Organization has resources for governance board and schema registry | No dedicated integration architecture capacity | Managed iPaaS with built-in mapping (Workato, Boomi) |

## Cross-System Comparison

### How Each ERP Maps to Canonical Entities

| Capability | SAP S/4HANA | NetSuite | Dynamics 365 | Salesforce | Oracle ERP Cloud |
|---|---|---|---|---|---|
| Customer entity name | BusinessPartner (BP) | customer | Account | Account | HZ_PARTIES |
| Customer ID format | 10-digit zero-padded | Integer | GUID | 18-char alphanumeric | Number |
| Address model | Complex (time-dependent, multi-use) | Sublist (addressbook) | Separate entity (LogisticsPostalAddress) | Compound fields (Billing/Shipping) | HZ_LOCATIONS (shared) |
| Order header | A_SalesOrder | salesOrder | SalesOrderHeader | Opportunity or Order | DOO_ORDER_HEADERS |
| Line item model | A_SalesOrderItem | salesOrderItem.item | SalesOrderLine | OpportunityLineItem or OrderItem | DOO_ORDER_LINES |
| Product/Item | A_Product + A_ProductPlant | item | InventTable / EcoResProduct | Product2 | EGP_SYSTEM_ITEMS |
| Invoice | A_BillingDocument | invoice | CustInvoiceJour | Invoice__c (custom) | RA_CUSTOMER_TRX |
| Amount precision | Up to 3 decimals (currency-dependent) | 2 decimals (default) | Up to 10 decimals | 2 decimals (default) | Currency-dependent |
| DateTime format | YYYYMMDD (legacy) or ISO 8601 | ISO 8601 | ISO 8601 | ISO 8601 | ISO 8601 |
| Null handling | Initial values ('' for string, 0 for number) | null | null | null | null |
| Enum representation | Domain values (coded) | Picklist references (internalId) | OptionSet (integer) | Picklist (string API name) | Lookup codes (varchar) |

[src2, src4, src5]

## Important Caveats

- **Canonical model is not MDM**: A CDM standardizes the schema; MDM deduplicates records, creates golden records, and manages survivorship. They are complementary, not interchangeable. Confusing them is the most common architectural mistake.
- **Governance overhead is real**: Without a governance board, canonical schemas diverge into "canonical" in name only — each team extends the model independently, producing multiple incompatible versions.
- **Performance impact of double transformation**: Every data exchange goes through two translations (source-to-canonical, canonical-to-target). For latency-sensitive real-time integrations (<100ms), consider direct mapping for that specific flow while maintaining canonical for batch/event flows.
- **Organizational alignment matters more than technology**: The biggest risk is not schema design but organizational buy-in. If domain teams refuse to adopt canonical adapters, the model becomes shelfware. Start with a high-visibility, high-pain integration to demonstrate value.
- **Not all entities deserve canonical treatment**: Low-volume, system-specific entities (e.g., Salesforce Campaigns, SAP Profit Centers) may not justify canonical modeling. Apply CDM selectively to high-frequency, cross-system entities.
- **Schema registries add operational complexity**: Running a schema registry (Confluent, Apicurio, AWS Glue) requires infrastructure, monitoring, and expertise. For teams without Kafka/event streaming, JSON Schema files in version control with CI/CD validation checks may suffice.

## Related Units

- [Field Mapping & Data Type Comparison Across ERPs](/business/erp-integration/field-mapping-data-type-comparison/2026)
- [Master Data Management: Golden Record, Survivorship Rules & Sync](/business/erp-integration/master-data-management-erp/2026)
- [Change Data Capture Patterns for ERP Integration](/business/erp-integration/change-data-capture-erp/2026)
- [Error Handling & Dead Letter Queue Patterns](/business/erp-integration/error-handling-dead-letter-queues/2026)
- [Order-to-Cash Integration Playbook](/business/erp-integration/order-to-cash-integration/2026)
- [Procure-to-Pay Integration Playbook](/business/erp-integration/procure-to-pay-integration/2026)
- [Batch vs Real-Time Integration Pattern Selection](/business/erp-integration/batch-vs-realtime-integration/2026)
