ERP Integration Architecture

Design enterprise ERP integrations. Covers middleware patterns, SAP/Oracle integration, data synchronization, master data management, and integration testing for enterprise systems.

ERP integration architecture is the design, configuration, and operational blueprint for connecting an enterprise resource planning (ERP) system with other enterprise systems—such as CRM, HRIS, e-commerce, supply chain, and analytics platforms. It matters when data must flow reliably between systems, when business processes span multiple applications, and when real-time or near-real-time consistency is required across the enterprise. This page describes the essential patterns, configurations, and failure modes of ERP integration architecture, with focus on what engineers must understand and do to build, monitor, and maintain integrations that do not break in production.

Data Flow Patterns

Point-to-Point Integration

A direct, one-to-one connection between two systems, such as ERP and CRM, is the simplest integration pattern. It relies on HTTP endpoints, often REST or SOAP, with synchronous or asynchronous message passing.

Synchronous request-response uses POST /api/v1/orders with a JSON payload:

{
  "orderNumber": "ORD-2023-00123",
  "customerId": 45029,
  "items": [
    {
      "productId": 1001,
      "quantity": 3,
      "unitPrice": 29.95
    }
  ],
  "shippingAddress": {
    "street": "123 Main St",
    "city": "Seattle",
    "postalCode": "98101"
  }
}

The ERP system returns a 201 Created response with the new order’s internal ID:

{
  "erpId": 87104,
  "status": "Confirmed",
  "createdAt": "2023-10-05T14:30:00Z"
}

The caller must handle timeouts (default 30s), retryable errors (503 Service Unavailable), and idempotency via X-Request-ID headers. The ERP service must validate the payload schema, enforce business rules (e.g., customerId must exist), and respond with 400 Bad Request and a validation error body:

{
  "error": "ValidationFailed",
  "details": [
    {
      "field": "items[0].quantity",
      "message": "Quantity must be at least 1",
      "value": 0
    },
    {
      "field": "shippingAddress.postalCode",
      "message": "Invalid postal code format",
      "value": "981"
    }
  ]
}

Asynchronous event-driven integration uses message queues such as RabbitMQ or Kafka. The ERP publishes an order.created event to a topic:

{
  "eventId": "evt-7a2d1f8e-9b4c-5d6e-7f8a-9b0c1d2e3f4a",
  "eventType": "order.created",
  "timestamp": "2023-10-05T14:30:00Z",
  "data": {
    "orderNumber": "ORD-2023-00123",
    "customerId": 45029,
    "totalAmount": 89.85,
    "status": "Created"
  }
}

Consumers (e.g., inventory system, billing service) subscribe to this topic and process events independently. Each event must be idempotent: the same message can be delivered multiple times, and processing it twice must not cause side effects. Consumers acknowledge messages using ack after successful processing; unacknowledged messages are redelivered on failure.

Hub-and-Spoke with Integration Platform

In larger environments, a central integration platform (such as MuleSoft, Apache Camel, or a custom Node.js service) acts as a hub, connecting multiple systems as spokes. The platform manages message routing, transformation, and orchestration.

The platform listens on a single HTTP endpoint (/integrate/endpoint/{name}) and routes messages based on the Content-Type and a X-Integration-Flow header:

POST /integrate/endpoint/salesforce-crm
Host: integration.example.com
Content-Type: application/json
X-Integration-Flow: customer.create
Authorization: Bearer abc123

{
  "name": "Acme Corp",
  "contactEmail": "[email protected]",
  "industry": "Technology",
  "billingAddress": {
    "city": "Austin",
    "state": "TX"
  }
}

The platform routes the message to a flow named customer.create, which performs the following steps:

  1. Transform the incoming JSON to a canonical format using a JSON-to-XML mapping.
  2. Validate the data against a schema (customer.schema.json).
  3. Call the ERP’s customer.create endpoint with POST /api/v1/customers.
  4. If the ERP returns 422 Unprocessable Entity, the platform logs the error and retries with exponential backoff.

The flow is configured via a JSON file (flows/customer.create.json):

{
  "name": "customer.create",
  "steps": [
    {
      "type": "transform",
      "input": "json",
      "output": "xml",
      "mapping": "customer/json-to-xml.xsl"
    },
    {
      "type": "validate",
      "schema": "customer.schema.json",
      "failOnInvalid": true
    },
    {
      "type": "call",
      "endpoint": "https://erp.example.com/api/v1/customers",
      "method": "POST",
      "headers": {
        "Authorization": "Bearer {{erp.auth.token}}"
      },
      "timeout": 60
    },
    {
      "type": "retry",
      "maxRetries": 3,
      "backoff": "exponential",
      "baseDelay": 1000,
      "maxDelay": 30000
    }
  ]
}

Each step can fail independently, and the platform records execution traces in a database table integration_runs with columns: id, flow_name, start_time, end_time, status, error_message, and payload.

Bi-Directional Synchronization

When data must flow both ways—such as customer records updated in CRM and reflected in ERP—the integration must handle bidirectional sync. Two strategies are common: change data capture (CDC) and polling.

Change Data Capture relies on the ERP database to emit change events for every update to a tracked table. For instance, when a customer is updated in the ERP, a trigger inserts a row into a cdc_events table:

INSERT INTO cdc_events (table_name, operation, row_id, old_data, new_data, timestamp)
VALUES (
  'customers',
  'UPDATE',
  1005,
  '{"name": "Acme Inc", "email": "[email protected]"}',
  '{"name": "Acme Corp", "email": "[email protected]", "industry": "Technology"}',
  '2023-10-05T14:31:20Z'
);

A sync process polls this table every 30 seconds:

curl -X GET 'https://integration.example.com/api/v1/cdc?since=2023-10-05T14:30:00Z'

The response includes all events since the last sync:

{
  "events": [
    {
      "id": 1587,
      "table": "customers",
      "operation": "UPDATE",
      "rowId": 1005,
      "oldData": {
        "name": "Acme Inc",
        "email": "[email protected]"
      },
      "newData": {
        "name": "Acme Corp",
        "email": "[email protected]",
        "industry": "Technology"
      },
      "timestamp": "2023-10-05T14:31:20Z"
    }
  ],
  "lastProcessed": "2023-10-05T14:31:20Z"
}

The sync process applies changes to the target system (CRM) and updates a sync_checkpoint table with the last processed timestamp.

Polling is simpler: the integration regularly fetches data from the ERP via a REST API. A scheduled job runs:

curl -H "Authorization: Bearer ${ERP_TOKEN}" \
     "https://erp.example.com/api/v1/customers?since=2023-10-05T14:30:00Z"

The ERP returns a paginated list of updated customers:

{
  "data": [
    {
      "customerId": 1005,
      "name": "Acme Corp",
      "lastUpdated": "2023-10-05T14:31:20Z"
    }
  ],
  "pagination": {
    "nextPage": "https://erp.example.com/api/v1/customers?since=2023-10-05T14:31:20Z&limit=100"
  }
}

The integration must handle pagination, deduplicate records, and update timestamps in the local database.

Error Handling and Resilience

Retry Strategies

Integrations fail. The most common failure is a transient network issue: the ERP service is down for 5 minutes during a peak order spike. The integration must retry failed calls using a combination of exponential backoff and circuit breaking.

A retry strategy is defined in configuration:

{
  "retry": {
    "maxAttempts": 5,
    "backoff": "exponential",
    "baseDelay": 1000,
    "multiplier": 2,
    "maxDelay": 30000,
    "retryOn": [
      "503",
      "504",
      "408",
      "429"
    ],
    "retryOnStatus": [
      408,
      429,
      503,
      504
    ],
    "retryOnException": [
      "NetworkException",
      "TimeoutException"
    ]
  }
}

On first failure (e.g., 503 Service Unavailable), the system waits 1s before retrying. On second failure, it waits 2s, then 4s, 8s, and 16s for subsequent attempts. If all attempts fail, the system logs a IntegrationFailed event and sends an alert via webhook.

Dead Letter Queues

When an integration step fails irrecoverably, the message is moved to a dead letter queue (DLQ). The DLQ is a dedicated queue for failed messages, often implemented using Kafka, RabbitMQ, or a database table.

A failed customer creation message in the DLQ might look like:

{
  "messageId": "msg-7a2d1f8e-9b4c-5d6e-7f8a-9b0c1d2e3f4a",
  "originalPayload": {
    "name": "Acme Corp",
    "contactEmail": "[email protected]"
  },
  "error": {
    "code": "ERP_CONNECTION_FAILED",
    "message": "Unable to connect to ERP API at https://erp.example.com",
    "details": {
      "host": "erp.example.com",
      "port": 443,
      "timeout": 30,
      "error": "Connection timed out"
    }
  },
  "attemptCount": 5,
  "failedAt": "2023-10-05T14:35:42Z",
  "retries": [
    {
      "attempt": 1,
      "durationMs": 1500,
      "error": "503 Service Unavailable"
    },
    {
      "attempt": 2,
      "durationMs": 3000,
      "error": "503 Service Unavailable"
    }
  ]
}

A monitoring service checks the DLQ every 10 minutes and alerts when more than 10 messages accumulate. A human can inspect and reprocess failed messages using a CLI:


curl -X POST -d '{}' 'https://integration.example.com/api/v1/dlq/requeue/msg-7a2d1f8e-9b4c-5d6e-7f8a-9b0c1d2e3f4a'

# Retry a message with updated payload
curl -X POST -d '{
  "name": "Acme Corp",
  "contactEmail": "[email protected]",
  "industry": "Software"
}' 'https://integration.example.com/api/v1/dlq/retry/msg-7a2d1f8e-9b4c-5d6e-7f8a-9b0c1d2e3f4a'

Idempotency and Duplicate Processing

Messages can be delivered multiple times. The integration must be idempotent: processing the same message twice must result in the same state.

The key is using a message_id field in the message header and a processed_messages table:

CREATE TABLE processed_messages (
  message_id VARCHAR(100) PRIMARY KEY,
  flow_name VARCHAR(50),
  processed_at TIMESTAMP,
  status VARCHAR(20),
  error TEXT
);

Before processing a message, the integration checks if message_id exists. If it does, it skips processing and logs Message already processed. If not, it inserts the record and proceeds.

Without idempotency, duplicate customer creation leads to multiple customer records with the same name and email, causing data drift and reconciliation issues.

Monitoring and Observability

Integrations must be monitored for latency, error rates, and throughput. Key metrics include:

A dashboard shows the integration health via:

Alerts trigger when:

Alerts are sent via email and Slack webhook:

{
  "text": "Integration: customer.create is failing",
  "attachments": [
    {
      "title": "Error Summary",
      "fields": [
        {
          "title": "Error Rate",
          "value": "7.2%",
          "short": true
        },
        {
          "title": "Last 10 Errors",
          "value": "503, 500, 408, 503, 500, 408, 503, 500, 408, 503",
          "short": true
        }
      ]
    }
  ]
}

Common Failure Modes

Silent Failures in Polling

Polling integrations often fail silently: the job runs, but no data is fetched. The most common cause is a misconfigured since timestamp. The integration uses 2023-10-05T14:30:00Z, but the ERP’s last_updated column is in TIMESTAMP WITH TIME ZONE, and the query is executed in UTC while the ERP uses America/Los_Angeles.

The fix is to ensure consistent time zone handling:

SELECT * FROM customers
WHERE last_updated >= '2023-10-05T21:30:00Z'

Failure to convert time zones leads to missed records and duplicated data.

Schema Mismatches in Message Transformation

When transforming data between systems, schema mismatches often go unnoticed. A field in the ERP is VARCHAR(50) but the integration expects TEXT. A value with 60 characters fails to insert, and the error is logged but not monitored.

The integration must validate schema at every transformation step. A missing field (customer.phone not present in incoming JSON) is handled by default values or by requiring the field in the schema.

Configuration Drift

Configurations drift across environments: staging uses a different ERP_TOKEN than production, and the integration platform runs on different versions of the flows/customer.create.json file. The solution is to version and deploy configurations via a Git repository:

git clone https://git.example.com/integration-configs.git
cd integration-configs
git checkout v1.3.2
cp flows/* /app/config/
systemctl reload integration-platform

The integration platform reads configuration from /app/config, and changes are applied via CI/CD.

Message Format Inconsistencies

Messages are often written with inconsistent formatting: some use snake_case, others camelCase, and some use PascalCase. The integration must normalize the format during transformation. A common failure is missing or misnamed fields: customer.name instead of customer.firstName, or order.total instead of order.totalAmount.

The integration logs all field mappings and missing fields:

{
  "warnings": [
    {
      "field": "order.total",
      "message": "Field 'total' not found in schema; using 'totalAmount' as fallback"
    },
    {
      "field": "customer.phone",
      "message": "Field 'phone' missing; using 'phoneNumber' instead"
    }
  ]
}

These warnings are collected and reviewed monthly to refine integration configuration.

Summary

ERP integration architecture is not a static diagram but a dynamic system of data flow, error handling, and observability. Engineers must design for idempotency, configure robust retry strategies, and monitor for common failure modes. The integration is only as reliable as its ability to handle network failures, schema changes, and configuration drift. The real test is not a successful deployment but a production system that continues to work when the ERP is down, when messages arrive out of order, and when data flows between systems without human intervention.

This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.