});
// Layer 3: Delivery β idempotent ERP submission with retry
const deliveryWorker = new Worker('delivery', async (job) => {
const acquired = await redis.set(
erp:submitted:${job.data.idempotencyKey},
'1', { NX: true, EX: 86400 }
);
if (!acquired) return { status: 'duplicate' };
await submitToERP(job.data.erpPayload);
}, { connection, concurrency: 5 });
Enter fullscreen mode Exit fullscreen mode
Keep these tiers separate. A field mapping change in Layer 2 should never touch Layer 1. A rate limit change in Layer 3 should never force re-running Layer 2.
1. Delta Inventory Sync with Watermark Never query all SKUs on every sync cycle. Query the ERP change journal for movements since the last watermark:
jsasync function syncInventoryFromERP(shop) {
const lastSyncedAt = await redis.get(erp:inventory:watermark:${shop})
|| '1970-01-01T00:00:00Z';
const changedItems = await erpClient.getInventoryChanges({
since: lastSyncedAt,
warehouseId: process.env.ERP_WAREHOUSE_ID,
});
if (changedItems.length === 0) return { synced: 0 };
const updates = (await Promise.all(
changedItems.map(async (item) => {
const shopifyItemId = await lookupShopifyInventoryItemId(shop, item.erpItemCode);
if (!shopifyItemId) { await logUnmappedItem(shop, item.erpItemCode); return null; }
return { inventory_item_id: shopifyItemId, available: item.availableQuantity };
})
)).filter(Boolean);
// Batch submit: max 100 items per Shopify API call
for (let i = 0; i < updates.length; i += 100) {
await shopifyAdmin.post('/inventory_bulk_adjust_quantity_at_location.json', {
location_id: process.env.SHOPIFY_LOCATION_ID,
changes: updates.slice(i, i + 100),
});
await new Promise(r => setTimeout(r, 500)); // Rate limit buffer
}
// Advance watermark AFTER successful sync β never before
await redis.set(erp:inventory:watermark:${shop}, new Date().toISOString());
return { synced: updates.length };
}
Enter fullscreen mode Exit fullscreen mode
Advance the watermark only after a successful sync. Advancing before processing means a mid-sync crash silently skips records.
1. Preventing Sync Loops Bidirectional sync creates infinite loops. Your middleware writes a price to Shopify. Shopify fires products/update. Your middleware ingests it and syncs back to the ERP. Repeat forever. The fix is one field:
js// Tag every middleware-originated write to Shopify
await shopifyAdmin.put(/products/${productId}.json, {
product: {
id: productId,
metafields: [{
namespace: 'integration',
key: 'source',
value: 'erp_middleware',
type: 'single_line_text_field',
}]
}
});
// Check the tag in every webhook handler before routing to ERP
async function handleProductUpdate(shop, payload) {
const source = payload.metafields?.find(
m => m.namespace === 'integration' && m.key === 'source'
)?.value;
if (source === 'erp_middleware') {
return { status: 'skipped', reason: 'middleware-originated write' };
}
await routeToERP(shop, payload);
}
Enter fullscreen mode Exit fullscreen mode
1. Error Classification That Prevents Incidents
Every ERP integration error falls into one of three categories. Handle each differently:
Error Type
HTTP Codes
Action
Alert?
Transient
408, 429, 500, 502, 503, 504
Retry with exponential backoff
Only on retry exhaustion
Data error
400, 404, 422
Route to DLQ immediately, no retry
Yes β data team
System error
401, 403
Pause queue, halt processing
Yes β ops team, P0
Retrying a data error wastes resources and delays the fix. Not retrying a transient error causes unnecessary data loss. Pausing on a system error prevents a queue backlog that compounds when the system recovers.
2. SAP Interface Layer Selection
Interface
Protocol
Recommended?
When to Use
SAP OData
REST / HTTP
Yes β preferred
All new integrations
SAP IDoc
Async file/message
With adapter
When OData unavailable
SAP BAPI via RFC
Proprietary RFC
No β avoid
Existing integrations only
SAP Integration Suite
Pre-built flows
Evaluate first
S/4HANA Cloud
The Integration Architecture Checklist
Before go-live, validate every item:
Source of truth defined per data domain in a shared document
SKU-to-ERP item code cross-reference table built and validated
Idempotency keys on every delivery worker call
Sync loop prevention via integration\_source metafield tagging
Watermark advanced only after successful sync, never before
Error classification routing: transient retry, data error DLQ, system error queue pause
Dead-letter queue includes full payload context and all retry timestamps
Shopify API rate limit respected between batch inventory writes
Full guide with complete field mapping code, delta sync implementation, SAP interface selection, pricing sync to Shopify B2B price lists, and conflict resolution patterns:
[](https://kolachitech.com/shopify-erp-integration-architecture/)
## [Shopify ERP Integration Architecture: A Technical Guide](https://kolachitech.com/shopify-erp-integration-architecture/)
Learn how to design Shopify ERP integration architecture for SAP, NetSuite, and Oracle. Covers data flow patterns, conflict resolution, field mapping, and fault tolerance.
 kolachitech.com