ERP Integration Anti-Patterns

Avoid the most costly mistakes in ERP integrations. Covers real-time vs batch pitfalls, data mapping failures, bi-directional sync traps, middleware selection mistakes, and the integration patterns that lead to project failures and data corruption.

ERP integration is the lifeblood of enterprise systems. When order data from a retail POS must sync to inventory in a warehouse, or when a customer invoice triggers a payment in accounts receivable, the integration pipeline must be reliable, performant, and observable. This page documents the most common anti-patterns that engineers and architects encounter when building and maintaining ERP integration systems—patterns that seem reasonable at first but degrade into complexity, latency, and unreliability. These anti-patterns arise in any environment where ERP systems (such as SAP, Oracle, Microsoft Dynamics, or custom solutions) exchange data with external systems—marketing, finance, supply chain, or e-commerce—via APIs, message queues, or file transfers.

Data Flow Design

Overloading a Single Integration Endpoint

A single HTTP endpoint, often exposed via a REST API, becomes the central hub for all data exchange between ERP and external systems. The endpoint is used to trigger all integration events: order creation, inventory updates, invoice posting, and customer master data synchronization.

The endpoint accepts a JSON payload with a type field and a data object:

{
  "type": "order.created",
  "data": {
    "orderId": "ORD-2024-001",
    "customerId": "CUST-1000",
    "items": [
      { "productId": "P-001", "quantity": 3, "unitPrice": 29.99 }
    ],
    "timestamp": "2024-04-15T12:00:00Z"
  }
}

This works until the ERP sends 500 messages per minute. The endpoint becomes a bottleneck. The integration service logs 500 internal server errors (500) in a single minute, but the logs show no clear correlation between errors and message content.

The root cause is that the endpoint assumes all events are processed synchronously and with equal priority. However, some events—like order creation—are critical, while others—like inventory updates—are best-effort. The service fails to distinguish between *event types*, leading to:

Poorly Designed Message Processing Loops

Messages are sent via a message queue (e.g., RabbitMQ, AWS SQS, or Kafka), but the consumer loop is naive. A typical consumer script reads messages in a tight loop:

while true; do
  curl -s http://queue-service:8080/next-message \
    -H "Authorization: Bearer $TOKEN" \
    -o /tmp/message.json
  if [ -s /tmp/message.json ]; then
    jq -r '.type' /tmp/message.json | xargs ./process-event.sh
  fi
  sleep 0.5
done

This loop fails silently when the queue service is slow or network-bound. The consumer polls every 500ms, but message delivery is bursty: 300 messages arrive in 10 seconds, then 300 more in the next 10 seconds. The loop processes the first 300 messages, but during the second burst, the curl command times out, and the consumer misses messages.

The problem is that the loop does not acknowledge messages after processing. If the consumer crashes after processing a message but before acknowledging it, the message is redelivered. But with sleep 0.5, a crash during sleep means the message is processed twice.

The solution is to use a message queue’s native acknowledgment mechanism. For example, in RabbitMQ, messages should be acknowledged after successful processing:


curl -X POST http://queue-service:8080/ack \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"messageId": "msg-123"}'

Failure to do so leads to duplicate processing, inconsistent state, and data drift.

Error Handling and Observability

Ignoring Idempotency in Integration Events

Integration events are sent from ERP to external systems with unique IDs. The receiving system processes the event and updates a database. However, the event handler does not ensure idempotency. The same event ID is sent multiple times, and the handler processes it each time.

A common error occurs when the ERP system retries a failed integration after 5 minutes. The integration logs show:

2024-04-15 12:00:00 | Order created: ORD-2024-001
2024-04-15 12:05:00 | Order created: ORD-2024-001 (duplicate)

The root cause is that the event handler does not check for existing records before inserting. The orders table has a processed_at column, but the handler writes to it without a WHERE clause. The INSERT INTO orders query runs without a ON CONFLICT DO NOTHING clause.

To fix this, the handler should check if the event has already been processed:

SELECT id FROM integration_events WHERE event_id = 'ORD-2024-001';

If the event exists, the handler returns early. Otherwise, it proceeds with processing and inserts the event.

Without idempotency, the system creates duplicate orders, updates inventory twice, and posts duplicate invoices.

Missing or Misused Retry Mechanisms

Integration systems use retry logic, but the configuration is inconsistent. A typical retry policy is defined in a JSON file:

{
  "retry": {
    "maxAttempts": 3,
    "backoff": {
      "type": "exponential",
      "initialDelay": 1000,
      "multiplier": 2,
      "maxDelay": 30000
    },
    "retryOn": ["500", "503", "408", "429"],
    "timeout": 15000
  }
}

The integration service uses this configuration to retry failed HTTP requests. However, the retry logic is not applied to message processing. The service retries the HTTP call to the ERP when it fails, but does not retry the entire message processing pipeline.

The service logs:

ERROR: Failed to process order event: HTTP 503 (Service Unavailable)
INFO: Retrying message after 1s
INFO: Retrying message after 2s
INFO: Retrying message after 4s
ERROR: Final attempt failed after 15s

But the message is not requeued after the final retry. The integration system assumes that a failed message is automatically retried, but it is not. The message is processed only once, and when it fails, it is lost.

The fix is to use a retry queue: a dead-letter queue (DLQ) for messages that fail after maxAttempts. The message is moved to the DLQ after the final retry, where it can be inspected manually or processed by a separate service.

The DLQ should contain the original message, the error log, and the number of attempts. The system should also track the failure reason—e.g., timeout, network, validation, business_logic—and use this to route messages to different recovery workflows.

Data Modeling and Transformation

Flat Data Structures in Complex Integrations

ERP systems store data in rich, hierarchical structures. A customer master record includes addresses, payment terms, tax codes, and custom fields. But the integration layer flattens this into a single JSON object:

{
  "customerId": "CUST-1000",
  "name": "Acme Corp",
  "billingAddressLine1": "123 Main St",
  "billingAddressCity": "Anytown",
  "billingAddressState": "CA",
  "shippingAddressLine1": "456 Oak Ave",
  "shippingAddressCity": "Othertown",
  "shippingAddressState": "NY",
  "paymentTerms": "Net 30",
  "taxCode": "TAX-01",
  "customField1": "Premium",
  "customField2": "2024"
}

This structure works for simple integrations but breaks when the ERP adds a new address type—e.g., a warehouseAddress—or when the external system expects nested data.

The integration fails silently when a new address is added but not mapped. The external system logs:

WARNING: Unknown field 'warehouseAddressLine1' in customer record
INFO: Ignoring unknown fields: [warehouseAddressLine1, warehouseAddressCity, warehouseAddressState]

The root cause is that the integration service uses a fixed schema with no schema evolution. The service expects a specific set of fields, and any field not in the schema is ignored.

The solution is to use a schema registry (e.g., Confluent Schema Registry, Avro, or Protobuf) or to use a flexible mapping layer that can handle nested and optional fields. The service should validate the incoming data against a schema and log warnings for unknown fields.

Inconsistent Data Mapping Between Systems

Data is transformed between ERP and external systems using mapping rules. The rules are defined in a configuration file:

mappings:
  order:
    source: "erp.orders"
    target: "external.orders"
    fields:
      orderId: "order_id"
      customerId: "customer_id"
      orderDate: "created_at"
      totalAmount: "sum_amount"
      status: "status_code"
    transforms:
      - name: "mapStatus"
        type: "lookup"
        sourceField: "status"
        lookupTable: "order_status_map"
        targetField: "status_code"
      - name: "addCurrencyCode"
        type: "constant"
        value: "USD"
        targetField: "currency"

The integration service applies these mappings during data transformation. However, the service does not validate the mapping rules at startup.

The system runs for weeks, and then a new order arrives with status: "shipped" but the external system shows status_code: "shipped" as null.

The cause is that the order_status_map table contains entries for new, confirmed, and invoiced, but not shipped. The lookup transform fails silently, returning null for unmapped values.

The solution is to validate the mapping rules at runtime. The service should:

The service should also expose a health endpoint that reports mapping health:

GET /health/mappings
{
  "status": "ok",
  "mappings": [
    {
      "name": "order",
      "valid": true,
      "errors": [
        { "field": "status", "message": "Missing value 'shipped' in lookup table" }
      ]
    }
  ]
}

Configuration and Deployment

Hardcoded Integration Configuration

Integration services use configuration files, but the configuration is hardcoded in the codebase. The config.json file is checked into Git, and the service reads it at startup:

{
  "erp": {
    "baseUrl": "https://erp.example.com",
    "username": "integration-user",
    "password": "secret",
    "apiVersion": "v2"
  },
  "external": {
    "baseUrl": "https://external.example.com",
    "apiKey": "abc123",
    "timeout": 10000
  }
}

The service is deployed to staging, but the staging environment uses a different ERP URL. The deployment process copies the config.json file to the server, but the file is not updated.

After a deployment, the integration fails with:

ERROR: HTTP 404 - Not Found
GET https://erp.example.com/v2/orders

The root cause is that the ERP service in staging is at https://staging-erp.example.com, but the config file still has https://erp.example.com.

The fix is to use environment variables to override the configuration:

export ERP_BASE_URL="https://staging-erp.example.com"
export EXTERNAL_API_KEY="xyz789"

The service should load configuration from:

  1. config.json
  2. Environment variables (prefixed with ERP_, EXTERNAL_)
  3. Command-line flags

The integration service should fail fast when required configuration values are missing.

Missing or Misconfigured Message Routing

Messages are routed through a series of services: ingestion, validation, transformation, and delivery. Each service is responsible for a phase of the integration. However, the routing is not clearly defined.

A message arrives at the ingestion service:

{
  "type": "order.created",
  "source": "d365",
  "data": { "orderId": "ORD-2024-001", "customerId": "CUST-1000" }
}

The service validates the message and sends it to the validation service via a message queue. But the validation service expects a different schema:

{
  "event": "order.created",
  "payload": {
    "orderId": "ORD-2024-001",
    "customerId": "CUST-1000"
  }
}

The validation service logs:

ERROR: Missing field 'event' in message
INFO: Failed to parse message: 'event' is required

The root cause is that the routing logic does not transform the message between services. The ingestion service sends the message as-is, but the validation service expects a different structure.

The solution is to define a message routing contract: a shared schema for message flow. Each service should validate the incoming message against the contract and transform it if needed.

The routing contract should be versioned and stored in a shared repository. Services should include the contract version in the message:

{
  "contract": "v1.2",
  "type": "order.created",
  "data": { ... }
}

The integration system should fail when the contract version is not supported.

Summary

ERP integration anti-patterns are not about technology choices—they are about *how* systems interact. They reveal themselves through subtle failures: duplicate events, unprocessed messages, inconsistent data, and silent configuration drift. Engineers must look beyond the obvious to discover that idempotency, retry logic, and message routing are not automatic but must be explicitly designed. The integration pipeline is not a black box; it is a series of conscious decisions, each vulnerable to a common pitfall.

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.