Canonical Data Model Design for Multi-ERP Integration

Type: ERP Integration Systems: SAP S/4HANA, NetSuite, Dynamics 365, Salesforce, Oracle ERP Cloud Confidence: 0.85 Sources: 7 Verified: 2026-03-07 Freshness: evolving

TL;DR

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.

SystemRoleAPI SurfaceDirection
SAP S/4HANAERP — financial/logistics masterOData v4, RFC/BAPIBidirectional
Oracle NetSuiteERP — mid-market financial/inventoryREST, SuiteTalk, SuiteQLBidirectional
Microsoft Dynamics 365 F&OERP — finance/supply chainOData v4, Web API, DMFBidirectional
SalesforceCRM — customer/opportunity masterREST v62.0, Composite, Platform EventsBidirectional
Oracle ERP CloudERP — enterprise financial/procurementREST, FBDI, Business EventsBidirectional
iPaaS / MiddlewareIntegration orchestratorVariesOrchestrator

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.

SystemPrimary API for ReadPrimary API for WriteEvent/CDC SupportCanonical Adapter Complexity
SAP S/4HANAOData v4 (CDS views)OData v4, BAPIBusiness Events, AIFHigh — deep customization, variant fields
NetSuiteSuiteQL, RESTSuiteTalk SOAP, RESTUser Event Scripts, SuiteScriptMedium — flexible but governance-limited
Dynamics 365Web API (OData v4)Web API, DMFDataverse webhooks, Dual WriteMedium — OData well-structured
SalesforceREST API, SOQLREST API, CompositePlatform Events, CDCMedium — well-documented but governor limits
Oracle ERP CloudREST APIREST API, FBDIBusiness Events, BICCHigh — 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

SystemEffective Read RateEffective Write RateBulk ImportBottleneck
SAP S/4HANA~5,000 records/min (OData)~1,000 records/min (BAPI)IDocs (batch), BAPI massCustom 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

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.

SystemRecommended AuthToken LifetimeNotes
SAP S/4HANAOAuth 2.0 (SAP BTP) or X.509Session-basedPrincipal propagation for user-context
NetSuiteToken-Based Authentication (TBA)Persistent tokensOAuth 2.0 available but TBA simpler for S2S
Dynamics 365OAuth 2.0 Client Credentials1h access tokenAzure AD app registration required
SalesforceOAuth 2.0 JWT Bearer2h sessionConnected app with digital certificate
Oracle ERP CloudOAuth 2.0 or Basic AuthToken-basedBasic Auth still common for FBDI/ESS jobs

Authentication Gotchas

Constraints

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 3rd system 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 all) → Anti-pattern for >3 systems
│   ├── Federated (domain adapters, governance board) → Recommended
│   └── Hybrid (central canonical, domain adapters) → Good starting point
├── 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 canonical, transform at consumption

Quick Reference

Canonical Entity to System-Specific Mapping

Canonical EntitySAP S/4HANANetSuiteDynamics 365SalesforceOracle ERP Cloud
CustomerBusinessPartner (BP)customerAccount (Dataverse)Account (sObject)hz_parties / hz_cust_accounts
ProductA_ProductitemProduct (Released)Product2egp_system_items
OrderA_SalesOrdersalesOrderSalesOrderHeaderOpportunity / Orderdoo_order_headers
InvoiceBillingDocumentinvoiceCustInvoiceJourInvoice (custom)ra_customer_trx
PaymentFI Document (BKPF/BSEG)customerPaymentCustPaymJournalTransPayment (custom)ap_checks / ar_cash_receipts
EmployeeA_EmployeeemployeeHcmWorkerContact / Userper_all_people_f
Vendor/SupplierA_SuppliervendorVendTableAccount (Supplier type)poz_suppliers
AddressA_BPAddressaddress (subrecord)LogisticsPostalAddressAddress (compound)hz_locations

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]

# 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]

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"],
        "customerType": map_sf_type(sf_account.get("Type", "Prospect")),
        "primaryEmail": sf_account.get("PersonEmail"),
        "primaryPhone": normalize_e164(sf_account.get("Phone")),
        "currency": sf_account.get("CurrencyIsoCode", "USD"),
        "status": map_sf_status(sf_account.get("Account_Status__c", "Active")),
        "systemReferences": [{
            "system": "sfdc",
            "externalId": sf_account["Id"],
            "entityType": "Account",
        }],
        "schemaVersion": "1.0.0"
    }

Verify: Run adapter against 100 real 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]

# Register 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":"..."}'
# Expected: {"id": 1}

# Set BACKWARD compatibility (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"}'

Verify: Attempt to register an incompatible schema change — registry should reject it.

5. Implement canonical transformation pipeline

Wire adapters into your integration platform (iPaaS, Kafka, or custom middleware). [src1, src5]

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:
    adapter = ADAPTERS.get(source_system)
    canonical = adapter(payload)
    validate(instance=canonical, schema=CUSTOMER_SCHEMA)  # fail fast
    return canonical

Verify: Process a test event from each source system, confirm output validates.

6. Implement schema evolution workflow

When new fields are needed, follow the additive evolution process. [src7]

# Check compatibility before registering new version
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":"..."}'
# Expected: {"is_compatible": true}

Verify: Old adapter output (without new field) still validates against new schema version.

Code Examples

Python: Bidirectional adapter with canonical validation

# Input:  Source system payload (any connected ERP)
# Output: Validated canonical Customer or validated system-specific payload

class CanonicalAdapter:
    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 validate_canonical(self, canonical: dict) -> bool:
        try:
            validate(instance=canonical, schema=self.schema)
            return True
        except ValidationError as e:
            raise ValueError(f"Validation failed for {self.system}: {e.message}")

class SAPCustomerAdapter(CanonicalAdapter):
    SAP_STATUS_MAP = {"1": "active", "2": "blocked", "3": "inactive"}

    def to_canonical(self, bp: dict) -> dict:
        canonical = {
            "canonicalId": self._resolve_id(bp["BusinessPartner"]),
            "legalName": bp["BusinessPartnerFullName"],
            "customerType": "organization" if bp.get("BusinessPartnerCategory") == "2" else "individual",
            "status": self.SAP_STATUS_MAP.get(bp.get("AuthorizationGroup", "1"), "active"),
            "schemaVersion": "1.0.0"
        }
        self.validate_canonical(canonical)
        return canonical

JavaScript/Node.js: Schema version migration utility

// Input:  Canonical payload in schema v1.0.0
// Output: Canonical payload migrated to schema v1.1.0

const migrations = {
  "1.0.0->1.1.0": (payload) => ({
    ...payload,
    industryCode: null,  // New optional field
    schemaVersion: "1.1.0",
  }),
};

function migratePayload(payload) {
  const key = `${payload.schemaVersion}->1.1.0`;
  const migrated = migrations[key](payload);
  const validate = ajv.compile(schemas["1.1.0"]);
  if (!validate(migrated)) throw new Error(JSON.stringify(validate.errors));
  return migrated;
}

Data Mapping

Field Mapping Reference — Canonical Customer to System-Specific

Canonical FieldSAP S/4HANANetSuiteDynamics 365SalesforceGotcha
canonicalIdZ_CANONICAL_ID (custom)custentity_canonical_idcr_canonicalid (custom)Canonical_ID__cMust be indexed in every system for reverse lookup
legalNameBusinessPartnerFullNamecompanyNameName (Account)NameSAP max 81 chars, NetSuite max 83, SF max 255
customerTypeBusinessPartnerCategoryisPerson (boolean)RelationshipTypeRecordTypeIdSAP uses codes, NetSuite boolean, SF record types
primaryEmailEmailAddress (BP contact)emailEMailAddressPersonEmailSAP email on contact, not BP directly
currencyT001-WAERS (company code)currency.refNameTransactionCurrencyIdCurrencyIsoCodeSAP company code default vs per-record
statusAuthorizationGroupentityStatus.refNameStateCode (0/1)Account_Status__cEach system has different lifecycle states
taxIdTaxNumber1defaultTaxRegVATNumTax_ID__c (custom)Format varies by jurisdiction
addressesA_BPAddress (1:N, time-dependent)addressbook (sublist)LogisticsPostalAddressBillingAddress, ShippingAddressSAP has complex address time-dependency

Data Type Gotchas

Error Handling & Failure Points

Common Error Codes

ErrorSystemMeaningResolution
VALIDATION_FAILEDCanonicalPayload doesn't conform to schemaCheck adapter transformation — usually missing required field or wrong type
SCHEMA_VERSION_MISMATCHCanonicalschemaVersion doesn't match expectedRun migration utility to upgrade payload
DUPLICATE_CANONICAL_IDCanonicalSame canonicalId with different systemReferencesRoute to MDM for deduplication
ADAPTER_NOT_FOUNDPipelineNo adapter for source systemRegister adapter before connecting new system
INCOMPATIBLE_SCHEMASchema RegistryChange breaks backward compatibilityRevert; create new major version if needed
TRANSFORM_TIMEOUTAdapterAdapter took >30s to transformCheck source API response time; batch smaller
DEAD_LETTER_QUEUE_FULLPipelineDLQ capacity exceededAlert ops; investigate high failure rate root cause

Failure Points in Production

Anti-Patterns

Wrong: Basing canonical model on one ERP's schema

# BAD — Using Salesforce Account fields as 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
}
# Every other system must translate TO Salesforce's model.
# When SF changes a field, ALL adapters break.

Correct: Neutral canonical model from business semantics

# GOOD — Business-driven model independent of any system
canonical_customer = {
    "canonicalId": "urn:acme:customer:550e8400-e29b-41d4-a716-446655440000",
    "legalName": "Acme Corporation",
    "customerType": "organization",
    "status": "active",
    "systemReferences": [
        {"system": "sfdc", "externalId": "001xx000003DGbzAAG"},
        {"system": "sap-s4", "externalId": "0001000042"},
        {"system": "netsuite", "externalId": "12345"},
    ],
    "schemaVersion": "1.0.0"
}
# Each system maps to/from a neutral model.
# Changing one system only affects that system's adapter.

Wrong: Trying to model everything upfront

# BAD — 200+ fields in canonical Customer from day one
canonical_customer_schema = {
    "properties": {
        "customerId": {}, "legalName": {}, "tradingName": {},
        "dbaName": {}, "parentCompany": {}, "ultimateParent": {},
        "dunsNumber": {}, "sic_code": {}, "naics_code": {},
        # ... 180 more fields ...
    }
}
# 6 months of design meetings, no integration delivered.
# 80% of fields are empty in most systems.

Correct: Start with core attributes, extend additively

# GOOD — 12 core fields, extend per use case
canonical_customer_v1 = {
    "properties": {
        "canonicalId": {},      # Immutable identifier
        "legalName": {},        # Required
        "customerType": {},     # Required
        "primaryEmail": {},     # Optional
        "currency": {},         # Required
        "status": {},           # Required
        "addresses": {},        # Array
        "systemReferences": {}, # Cross-reference IDs
        "createdAt": {},        # Audit trail
        "schemaVersion": {},    # Evolution tracking
    }
}
# v1.1 adds: industryCode, taxId (optional, backward compatible)
# v2.0 (breaking): changes addressType enum — requires migration

Wrong: Central team owns all adapters

# BAD — Integration CoE (5 people) owns everything
# Canonical schema + ALL adapters + pipeline + monitoring
# Result: 5 people become bottleneck. Average lead time: 6 weeks.

Correct: Federated ownership with governance board

# GOOD — Domain teams own adapters, governance board owns canonical
# CRM Team: Salesforce adapter
# Finance Team: SAP + NetSuite adapters
# Platform Team: Schema registry + CI/CD + monitoring
# Governance Board: Weekly 30-min schema review
# Result: Average lead time for new field: 1-2 weeks.

Common Pitfalls

Diagnostic Commands

# Validate canonical payload against schema
ajv validate -s schemas/canonical/Customer.v1.json -d payload.json --all-errors
# Expected: payload.json valid

# List registered schema versions
curl -s http://schema-registry:8081/subjects/canonical-customer-value/versions | jq .
# Expected: [1, 2, 3]

# Check compatibility of 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}

# Query cross-references for a canonical ID
SELECT system, external_id, last_synced_at
FROM canonical_cross_references
WHERE canonical_id = 'urn:acme:customer:550e8400-...';

# Count validation failures in last 24h
SELECT source_system, COUNT(*) as failures
FROM canonical_validation_log
WHERE created_at > NOW() - INTERVAL '24 hours'
GROUP BY source_system ORDER BY failures DESC;

Version History & Compatibility

EraApproachSchema FormatGovernanceStatus
2003-2015Enterprise-wide XML canonicalXML Schema (XSD)Central governance committeeLegacy
2015-2020API-first canonical (REST/JSON)JSON Schema, OpenAPIAPI management platformMature
2020-2024Event-driven canonical (streaming)Avro, Protobuf, JSON SchemaSchema registryCurrent
2024-2026Domain-driven canonical (bounded contexts)JSON Schema + AsyncAPIFederated governance + CI/CDEmerging

Schema versions follow semantic versioning. Minor versions are perpetually backward compatible. Major versions receive minimum 90-day deprecation window with dual-version support. [src7]

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Integrating 3+ systems sharing Customer, Order, or Product dataOnly 2 systems with stable schemasDirect field mapping with transformation layer
Systems frequently replaced/upgraded (M&A, cloud migration)All systems from same vendor (e.g., all-SAP)Vendor-native integration (SAP CPI, Oracle SOA)
Multiple teams build integrations independentlySingle integration team handles all flowsPoint-to-point with shared mapping documentation
Event-driven architecture with Kafka/message busSimple batch ETL running once nightlyETL/ELT with source-to-target mapping
Long-term strategic integration (3+ year horizon)Short-term project or POC (< 6 months)Direct mapping — refactor to CDM later

Cross-System Comparison

How Each ERP Maps to Canonical Entities

CapabilitySAP S/4HANANetSuiteDynamics 365SalesforceOracle ERP Cloud
Customer entity nameBusinessPartner (BP)customerAccountAccountHZ_PARTIES
Customer ID format10-digit zero-paddedIntegerGUID18-char alphanumericNumber
Address modelComplex (time-dependent)Sublist (addressbook)Separate entityCompound fieldsHZ_LOCATIONS (shared)
Order headerA_SalesOrdersalesOrderSalesOrderHeaderOpportunity or OrderDOO_ORDER_HEADERS
Line item modelA_SalesOrderItemsalesOrderItem.itemSalesOrderLineOpportunityLineItemDOO_ORDER_LINES
Product/ItemA_Product + A_ProductPlantitemEcoResProductProduct2EGP_SYSTEM_ITEMS
Amount precisionUp to 3 decimals2 decimalsUp to 10 decimals2 decimalsCurrency-dependent
DateTime formatYYYYMMDD (legacy) / ISO 8601ISO 8601ISO 8601ISO 8601ISO 8601
Null handlingInitial values ('' / 0)nullnullnullnull
Enum representationDomain values (coded)Picklist refs (internalId)OptionSet (integer)Picklist (string name)Lookup codes (varchar)

Important Caveats

Related Units