<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sanya Mittal</title>
    <description>The latest articles on DEV Community by Sanya Mittal (@sanya_mittal_a509a2c50a2d).</description>
    <link>https://dev.to/sanya_mittal_a509a2c50a2d</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3523639%2Feaa41d0e-95c1-466f-863f-5fa6dcba3804.jpeg</url>
      <title>DEV Community: Sanya Mittal</title>
      <link>https://dev.to/sanya_mittal_a509a2c50a2d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sanya_mittal_a509a2c50a2d"/>
    <language>en</language>
    <item>
      <title>Making Salesforce API Integrations Safe to Retry</title>
      <dc:creator>Sanya Mittal</dc:creator>
      <pubDate>Mon, 02 Nov 2026 14:38:03 +0000</pubDate>
      <link>https://dev.to/sanya_mittal_a509a2c50a2d/making-salesforce-api-integrations-safe-to-retry-18ne</link>
      <guid>https://dev.to/sanya_mittal_a509a2c50a2d/making-salesforce-api-integrations-safe-to-retry-18ne</guid>
      <description>&lt;p&gt;An API request times out, but Salesforce may have already processed it. Your integration retries the request, and suddenly the same business transaction appears twice.&lt;/p&gt;

&lt;p&gt;This is a common integration-design problem. It becomes harder when multiple workers process the same event or external systems resend requests.&lt;/p&gt;

&lt;p&gt;It provides External ID fields and upsert operations to simplify record synchronization. However, upsert alone does not guarantee safe processing under concurrent requests.&lt;/p&gt;

&lt;p&gt;In this article, we examine how to design &lt;a href="https://www.oodles.com/salesforce/6007347?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=backlink&amp;amp;utm_content=devto_article_43" rel="noopener noreferrer"&gt;Salesforce integration solutions&lt;/a&gt; that handle retries, duplicate events, and concurrent updates. We will use REST API examples and discuss the trade-offs between synchronous requests, queues, and asynchronous processing.&lt;/p&gt;

&lt;h1&gt;
  
  
  Context and Setup
&lt;/h1&gt;

&lt;p&gt;A typical Crm integration connects an external application with Accounts, Contacts, Orders, or custom objects.&lt;/p&gt;

&lt;p&gt;Consider an order-management application sending order updates to it.&lt;/p&gt;

&lt;p&gt;Three conditions can create duplicate-write problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The external application retries after a network timeout.&lt;/li&gt;
&lt;li&gt;Two workers process the same event simultaneously.&lt;/li&gt;
&lt;li&gt;A message broker redelivers an event after an acknowledgment failure.
It supports External ID-based upsert operations. According to its developer documentation, Bulk API 2.0 is a suitable candidate for operations involving more than 2,000 records. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This provides a useful architectural distinction: small transactional updates and large data synchronization jobs should not necessarily follow the same processing strategy.&lt;br&gt;
Solution: Build Retry-Safe Integrations&lt;br&gt;
The objective is to make repeated delivery of the same business event safe without silently overwriting unrelated updates.&lt;/p&gt;

&lt;h1&gt;
  
  
  Step 1: Assign a Stable External ID
&lt;/h1&gt;

&lt;p&gt;Every synchronized business record should have a stable identifier originating from the source system.&lt;/p&gt;

&lt;p&gt;For example, create an External_Order_Id__c field on the Salesforce Order object.&lt;/p&gt;

&lt;p&gt;Configure it as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;External ID: Enabled&lt;/li&gt;
&lt;li&gt;Unique: Enabled&lt;/li&gt;
&lt;li&gt;Required: Where business rules permit
This gives the integration a consistent identifier instead of relying on Salesforce-generated record IDs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;REST API supports PATCH-based upsert using an External ID field. It creates a record when no match exists and updates an existing record when one match exists. &lt;/p&gt;

&lt;h1&gt;
  
  
  Step 2: Use Upsert Instead of Blind Inserts
&lt;/h1&gt;

&lt;p&gt;The following Node.js example sends an order through the REST API.&lt;br&gt;
async function syncOrder(order, accessToken, instanceUrl) {&lt;br&gt;
  // Use the source-system ID to identify the record.&lt;br&gt;
  const externalId = encodeURIComponent(order.externalOrderId);&lt;/p&gt;

&lt;p&gt;const url =&lt;br&gt;
    &lt;code&gt;${instanceUrl}/services/data/vXX.X/sobjects/&lt;/code&gt; +&lt;br&gt;
    &lt;code&gt;Order__c/External_Order_Id__c/${externalId}&lt;/code&gt;;&lt;/p&gt;

&lt;p&gt;const response = await fetch(url, {&lt;br&gt;
    method: "PATCH",&lt;br&gt;
    headers: {&lt;br&gt;
      Authorization: &lt;code&gt;Bearer ${accessToken}&lt;/code&gt;,&lt;br&gt;
      "Content-Type": "application/json"&lt;br&gt;
    },&lt;br&gt;
    body: JSON.stringify({&lt;br&gt;
      Status_&lt;em&gt;c: order.status,&lt;br&gt;
      Amount&lt;/em&gt;_c: order.amount&lt;br&gt;
    })&lt;br&gt;
  });&lt;/p&gt;

&lt;p&gt;// Surface failures instead of silently accepting unsuccessful writes.&lt;br&gt;
  if (!response.ok) {&lt;br&gt;
    throw new Error(&lt;code&gt;Salesforce sync failed: ${response.status}&lt;/code&gt;);&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;return response.status;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;The example assumes authentication and token refresh happen outside the function.&lt;/p&gt;

&lt;p&gt;A repeated request with the same External ID targets the same record. However, concurrent requests can still create race conditions or overwrite newer values.&lt;/p&gt;

&lt;p&gt;Therefore, record identity and event ordering require separate controls.&lt;br&gt;
Step 3: Control Concurrent Requests&lt;br&gt;
Serialize updates for the same external record when multiple workers can process overlapping events.&lt;/p&gt;

&lt;p&gt;A practical architecture uses a queue partitioned by the source-system record ID.&lt;br&gt;
const queues = new Map();&lt;/p&gt;

&lt;p&gt;async function processOrderEvent(order, token, instanceUrl) {&lt;br&gt;
  const key = order.externalOrderId;&lt;/p&gt;

&lt;p&gt;// Keep updates for the same order in sequence.&lt;br&gt;
  const previous = queues.get(key) || Promise.resolve();&lt;/p&gt;

&lt;p&gt;const current = previous&lt;br&gt;
    .catch(() =&amp;gt; {}) // Allow later events after a failed request.&lt;br&gt;
    .then(() =&amp;gt; syncOrder(order, token, instanceUrl));&lt;/p&gt;

&lt;p&gt;queues.set(key, current);&lt;/p&gt;

&lt;p&gt;try {&lt;br&gt;
    return await current;&lt;br&gt;
  } finally {&lt;br&gt;
    // Remove only the completed queue entry.&lt;br&gt;
    if (queues.get(key) === current) {&lt;br&gt;
      queues.delete(key);&lt;br&gt;
    }&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;This example provides in-process serialization, not distributed locking. Multiple application instances need a shared queue or partitioning mechanism.&lt;/p&gt;

&lt;p&gt;For higher-volume workloads, consider a durable message queue, retry policies, dead-letter handling, and event-version checks.&lt;/p&gt;

&lt;p&gt;The trade-off is operational complexity. Synchronous processing is simpler for low-volume updates. Queues provide better control when event volume and concurrency increase.&lt;/p&gt;

&lt;h1&gt;
  
  
  Real-World Application
&lt;/h1&gt;

&lt;p&gt;The same integration considerations matter when it supports multiple business processes and external reporting systems.&lt;/p&gt;

&lt;p&gt;In our Rise and Shine / ASUAire Salesforce engagement at &lt;a href="//oodles.com"&gt;Oodles&lt;/a&gt;, the documented scope included multi-region CRM customization, custom objects, automated sales workflows, and external reporting integrations.&lt;/p&gt;

&lt;p&gt;The architecture required consideration of how CRM data would move between operational workflows and connected reporting systems.&lt;/p&gt;

&lt;p&gt;Our implementation scope covered CRM customization and integration requirements. The available project documentation does not establish a specific duplicate-request incident or measured API performance improvement. We therefore do not attribute an invented response-time reduction to this project.&lt;/p&gt;

&lt;p&gt;For similar integration requirements, Oodles approaches data synchronization by defining record identity, ownership, update behavior, and failure handling before implementation.&lt;br&gt;
Conclusion: Design for Repeated Delivery&lt;br&gt;
Retry handling should be part of the integration architecture, not an afterthought added after production failures.&lt;/p&gt;

&lt;h1&gt;
  
  
  For integrations, focus on these five engineering decisions:
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Use unique External ID fields to identify records across systems.&lt;/li&gt;
&lt;li&gt;Prefer upsert over blind inserts for synchronization workflows.&lt;/li&gt;
&lt;li&gt;Serialize updates when multiple workers can modify the same record.&lt;/li&gt;
&lt;li&gt;Track event versions to prevent older events from overwriting newer data.&lt;/li&gt;
&lt;li&gt;Use Bulk API 2.0 for suitable large-volume operations and monitor failed records.
A successful API response is not the only measure of integration correctness. The system must also handle retries, concurrency, and partial failures predictably.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Have questions about API integration, retry handling, or synchronization architecture? Contact our &lt;a href="//oodles.com/contact-us"&gt;Salesforce&lt;/a&gt; experts to discuss your implementation requirements. Share your technical questions in the comments as well.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;What is an External ID in Salesforce?&lt;br&gt;
An External ID is a field that stores a record identifier from another system. Integrations use it to locate it's records without requiring Salesforce-generated IDs. External IDs support synchronization, upsert operations, and matching records across connected applications.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How does upsert work in Salesforce REST API?&lt;br&gt;
REST API upsert uses an External ID to determine whether a record already exists. It creates a record when no matching record exists and updates the matching record otherwise. Multiple matching records produce an error instead of an update.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How can developers prevent duplicate records in Salesforce integrations?&lt;br&gt;
Developers should use unique External ID fields, upsert operations, and stable source-system identifiers. They should also control concurrent requests for identical records. Distributed integrations need durable event tracking and appropriate retry handling.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When should developers use Salesforce Bulk API 2.0?&lt;br&gt;
Bulk API 2.0 suits large data operations involving thousands of records. It identifies operations exceeding 2,000 records as good candidates. It supports asynchronous insert, update, upsert, query, and delete operations, making it useful for large synchronization workloads.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Does Salesforce upsert guarantee idempotency?&lt;br&gt;
Upsert helps make repeated requests target the same record, but it does not guarantee complete application-level idempotency. Concurrent requests, duplicate external identifiers, and out-of-order updates still require additional controls, such as event versioning and request serialization.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>salesforce</category>
      <category>crm</category>
      <category>ai</category>
      <category>cpq</category>
    </item>
    <item>
      <title>How to Stop Overselling in Inventory Management Software Development (Node.js and PostgreSQL) published: false</title>
      <dc:creator>Sanya Mittal</dc:creator>
      <pubDate>Fri, 30 Oct 2026 12:02:18 +0000</pubDate>
      <link>https://dev.to/sanya_mittal_a509a2c50a2d/how-to-stop-overselling-in-inventory-management-software-development-nodejs-and-postgresql-25fi</link>
      <guid>https://dev.to/sanya_mittal_a509a2c50a2d/how-to-stop-overselling-in-inventory-management-software-development-nodejs-and-postgresql-25fi</guid>
      <description>&lt;p&gt;Two checkout requests arrive 4 ms apart for the last unit of a SKU. Both read &lt;code&gt;on_hand = 1&lt;/code&gt;, both pass the check in application code, and both write. Stock ends at -1 and one customer gets an apology email. This race appears wherever a web store, a marketplace sync, and a warehouse scanner write to the same row, and it grows worse with every added channel or worker process. It is the first bug we design against in &lt;a href="https://www.oodles.com/inventory-warehouse-management-/2172960/case-study/skulocity?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=backlink&amp;amp;utm_content=devto_article_42" rel="noopener noreferrer"&gt;inventory management software development for concurrent, multi-writer systems&lt;/a&gt;. The fix is small: let PostgreSQL decide the outcome, not your Node.js process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context and Setup
&lt;/h2&gt;

&lt;p&gt;The scenario is a Node.js 20 API with &lt;code&gt;pg&lt;/code&gt; talking to PostgreSQL 15, serving several channels that reserve stock before payment clears. PostgreSQL is a safe base for this: the 2025 Stack Overflow Developer Survey reports it as the most-used database among professional developers (55.6%).&lt;/p&gt;

&lt;p&gt;You need two tables. &lt;code&gt;stock_levels&lt;/code&gt; holds &lt;code&gt;sku&lt;/code&gt;, &lt;code&gt;location&lt;/code&gt;, &lt;code&gt;on_hand&lt;/code&gt;, and &lt;code&gt;reserved&lt;/code&gt;. &lt;code&gt;reservations&lt;/code&gt; holds a unique &lt;code&gt;idempotency_key&lt;/code&gt;, &lt;code&gt;qty&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, and &lt;code&gt;expires_at&lt;/code&gt;. Available stock is &lt;code&gt;on_hand - reserved&lt;/code&gt;, and it must never go below zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reserving Stock Atomically in Inventory Management Software Development
&lt;/h2&gt;

&lt;p&gt;The short answer is one conditional &lt;code&gt;UPDATE&lt;/code&gt; guarded by a constraint. Three steps get you there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Make the database refuse negative stock
&lt;/h3&gt;

&lt;p&gt;In Inventory Management Software Development a constraint protects the invariant from every writer, including services that do not exist yet.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fix any existing rows that violate the rule.&lt;/li&gt;
&lt;li&gt;Add the constraint in a migration with a short lock timeout.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Why: even buggy code or a manual script cannot oversell past this line&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;stock_levels&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;available_not_negative&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;on_hand&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;reserved&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Reserve with One Conditional UPDATE in Inventory Management Software Development
&lt;/h3&gt;

&lt;p&gt;Check and write must be one statement. PostgreSQL locks the row during &lt;code&gt;UPDATE&lt;/code&gt;, and under READ COMMITTED a waiting transaction re-evaluates the &lt;code&gt;WHERE&lt;/code&gt; clause after the lock clears (see the PostgreSQL transaction isolation docs). The second request then matches zero rows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;pg&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;pg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reserve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ttlSeconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BEGIN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Why: the unique key turns a client retry into a no-op instead of a double hold&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`INSERT INTO reservations (idempotency_key, sku, location, qty, status, expires_at)
       VALUES ($1, $2, $3, $4, 'held', now() + make_interval(secs =&amp;gt; $5))
       ON CONFLICT (idempotency_key) DO NOTHING RETURNING id`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ttlSeconds&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hold&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rowCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;duplicate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Why: check and write in one statement avoids the read-then-write race&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;upd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`UPDATE stock_levels SET reserved = reserved + $3
       WHERE sku = $1 AND location = $2 AND on_hand - reserved &amp;gt;= $3`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;upd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rowCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;insufficient&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;COMMIT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reserved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hold&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Expire holds and weigh the alternatives
&lt;/h3&gt;

&lt;p&gt;Inventory Management Software Development Abandoned carts must release stock, or &lt;code&gt;reserved&lt;/code&gt; drifts upward. Run this worker query every minute.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Why: SKIP LOCKED lets several workers run without blocking each other&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;expired&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'held'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;LOCKED&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;flipped&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;reservations&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'expired'&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;expired&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;totals&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="c1"&gt;-- Why: UPDATE ... FROM applies only one source row per target row, so sum first&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;flipped&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;location&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;stock_levels&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;reserved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reserved&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;totals&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;location&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trade-offs against other designs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; first:&lt;/strong&gt; correct, but it adds a round trip and holds the lock longer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimistic version column:&lt;/strong&gt; cheap for cold SKUs, yet hot SKUs burn retries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis counters:&lt;/strong&gt; fast, but they create a second source of truth that needs reconciliation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The conditional update serializes writers per row. For a flash-sale SKU, split stock into bucket rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Application
&lt;/h2&gt;

&lt;p&gt;In one of our inventory management software development projects at &lt;a href="https://oodles.com" rel="noopener noreferrer"&gt;Oodles&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;In inventory Management Software Development put the invariant in the database with a &lt;code&gt;CHECK&lt;/code&gt; constraint, because application checks cannot see concurrent writers.&lt;/li&gt;
&lt;li&gt;Collapse check and write into one conditional &lt;code&gt;UPDATE&lt;/code&gt;; READ COMMITTED re-evaluates the &lt;code&gt;WHERE&lt;/code&gt; clause after a lock wait.&lt;/li&gt;
&lt;li&gt;Use an idempotency key so client and queue retries never double-reserve.&lt;/li&gt;
&lt;li&gt;Expire holds with &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt; workers, and sum quantities before updating the parent rows.&lt;/li&gt;
&lt;li&gt;Choose Redis or optimistic locking only after measuring contention on hot SKUs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Questions about reservation design or hot-SKU contention? Drop them in the comments, or reach us about &lt;a href="https://oodles.com/contact-us" rel="noopener noreferrer"&gt;inventory management software development&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is inventory management software development?&lt;/strong&gt;&lt;br&gt;
It is the engineering of systems that track stock across locations and channels, reserve units for orders, and keep counts consistent under concurrent writes. Core work covers data models, reservation logic, integrations, and audit trails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you prevent overselling in inventory software?&lt;/strong&gt;&lt;br&gt;
Combine a database &lt;code&gt;CHECK&lt;/code&gt; constraint with a single conditional &lt;code&gt;UPDATE&lt;/code&gt; that tests available stock in its &lt;code&gt;WHERE&lt;/code&gt; clause. The database serializes competing writers per row, so only requests that fit the remaining quantity succeed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I use Redis or PostgreSQL for stock counts?&lt;/strong&gt;&lt;br&gt;
In Inventory Management Software Development Use PostgreSQL as the source of truth because it gives transactions, constraints, and audit history. Add Redis only as a read cache or rate limiter, and reconcile it against the database to avoid drift.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which isolation level do stock updates need?&lt;/strong&gt;&lt;br&gt;
READ COMMITTED is enough for a conditional &lt;code&gt;UPDATE&lt;/code&gt;, because PostgreSQL re-checks the &lt;code&gt;WHERE&lt;/code&gt; clause after a lock wait. Use SERIALIZABLE only for multi-row invariants, and be ready to retry on serialization failures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do reservations need idempotency keys?&lt;/strong&gt;&lt;br&gt;
Clients, queues, and gateways retry failed calls. A unique idempotency key lets the second attempt hit &lt;code&gt;ON CONFLICT DO NOTHING&lt;/code&gt;, so one order can never hold stock twice.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>node</category>
      <category>architecture</category>
      <category>backend</category>
    </item>
    <item>
      <title>How to Make ERP Integration Services Retry-Safe in Node.js</title>
      <dc:creator>Sanya Mittal</dc:creator>
      <pubDate>Thu, 29 Oct 2026 08:28:37 +0000</pubDate>
      <link>https://dev.to/sanya_mittal_a509a2c50a2d/how-to-make-erp-integration-services-retry-safe-in-nodejs-4apa</link>
      <guid>https://dev.to/sanya_mittal_a509a2c50a2d/how-to-make-erp-integration-services-retry-safe-in-nodejs-4apa</guid>
      <description>&lt;p&gt;A webhook times out after 30 seconds, so the sender retries. Your handler had already created the sales order in the ERP, so the retry creates a second one. The warehouse then picks the same items twice. This is the most common failure we see when ERP integration services meet unreliable networks: at-least-once delivery on one side, a non-idempotent create endpoint on the other. Here is &lt;a href="https://www.oodles.com/erp-integration-services?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=backlink&amp;amp;utm_content=devto_article_41" rel="noopener noreferrer"&gt;how ERP integration services&lt;/a&gt; handle retries safely in production. We build a small Node.js and PostgreSQL write path that survives duplicate deliveries, process crashes, and ERP outages, then look at how the pattern applies on a live project.&lt;/p&gt;

&lt;h1&gt;
  
  
  Context and Setup
&lt;/h1&gt;

&lt;p&gt;Idempotency means that applying the same operation twice leaves the system in the same state as applying it once.&lt;/p&gt;

&lt;p&gt;Our scenario is a Node.js 20 service that turns storefront webhooks into ERP sales orders. You need PostgreSQL 14 or newer and an ERP endpoint with an external reference field. Postman’s State of the API Report (2025) found that 50.8% of developers cite unauthorized or excessive API calls as their top security concern. The finding centers on AI agents, but an unguarded retry loop creates the same traffic internally.&lt;/p&gt;

&lt;h1&gt;
  
  
  Fixing Duplicate Writes in ERP Integration Services
&lt;/h1&gt;

&lt;p&gt;The fix has three parts: a deterministic key, an atomic claim, and bounded retries.&lt;/p&gt;

&lt;h1&gt;
  
  
  Step 1: Build a Deterministic Key for Each ERP Write
&lt;/h1&gt;

&lt;p&gt;A deterministic key comes from the business event, so every retry produces the same value. A random ID per attempt makes each retry look like new work. Combine source system, event ID, and entity version.&lt;/p&gt;

&lt;p&gt;ts&lt;br&gt;
import { createHash } from "node:crypto";&lt;/p&gt;

&lt;p&gt;export function idempotencyKey(source: string, eventId: string, version: number): string {&lt;br&gt;
  // Why: same event, same key, even after a restart&lt;br&gt;
  return createHash("sha256").update(&lt;code&gt;${source}:${eventId}:${version}&lt;/code&gt;).digest("hex");&lt;br&gt;
}&lt;/p&gt;

&lt;h1&gt;
  
  
  Step 2: Claim the Key in Postgres Before the ERP Integration Services Call
&lt;/h1&gt;

&lt;p&gt;Claim the key atomically before any ERP write:&lt;/p&gt;

&lt;p&gt;Insert the key with ON CONFLICT DO NOTHING.&lt;br&gt;
Read the row status. Return early if it is done.&lt;br&gt;
Call the ERP, passing the key as external_ref.&lt;br&gt;
Mark the row done and store the ERP-assigned ID.&lt;br&gt;
sql&lt;br&gt;
CREATE TABLE erp_write_log (&lt;br&gt;
  idem_key   text PRIMARY KEY,            -- Why: rejects duplicate claims atomically&lt;br&gt;
  status     text NOT NULL DEFAULT 'pending',&lt;br&gt;
  erp_id     text&lt;br&gt;
);&lt;br&gt;
ts&lt;br&gt;
export async function handleEvent(evt: OrderEvent) {&lt;br&gt;
  const key = idempotencyKey("storefront", evt.id, evt.version);&lt;br&gt;
  // Why: avoids a check-then-insert race under concurrent webhooks&lt;br&gt;
  await pool.query("INSERT INTO erp_write_log (idem_key) VALUES ($1) ON CONFLICT DO NOTHING", [key]);&lt;br&gt;
  const { rows } = await pool.query("SELECT status FROM erp_write_log WHERE idem_key = $1", [key]);&lt;br&gt;
  if (rows[0].status === "done") return;  // Why: finished writes never rerun&lt;br&gt;
  // Why: a crash after the ERP write leaves 'pending'; a unique external_ref makes the retry safe&lt;br&gt;
  const created = await postOrder(evt.order, key);&lt;br&gt;
  await pool.query("UPDATE erp_write_log SET status = 'done', erp_id = $2 WHERE idem_key = $1", [key, created.id]);&lt;br&gt;
}&lt;/p&gt;

&lt;h1&gt;
  
  
  Step 3: Retry With Jitter and Know the Trade-Offs
&lt;/h1&gt;

&lt;p&gt;Bounded retries repeat only failures that can succeed later: 5xx and 429. Other 4xx responses mean bad input, so retrying only adds load.&lt;/p&gt;

&lt;p&gt;ts&lt;br&gt;
async function postOrder(order: Order, key: string) {&lt;br&gt;
  for (let attempt = 0; attempt &amp;lt; 5; attempt++) {&lt;br&gt;
    const res = await fetch(&lt;code&gt;${process.env.ERP_URL}/sales-orders&lt;/code&gt;, {&lt;br&gt;
      method: "POST",&lt;br&gt;
      headers: { "content-type": "application/json", "Idempotency-Key": key },&lt;br&gt;
      body: JSON.stringify({ ...order, external_ref: key }),&lt;br&gt;
    });&lt;br&gt;
    if (res.ok) return res.json();&lt;br&gt;
    if (res.status &amp;lt; 500 &amp;amp;&amp;amp; res.status !== 429) throw new Error(&lt;code&gt;ERP rejected: ${res.status}&lt;/code&gt;); // Why: bad input never heals&lt;br&gt;
    // Why: full jitter stops workers retrying in lockstep&lt;br&gt;
    await new Promise((r) =&amp;gt; setTimeout(r, Math.random() * Math.min(30_000, 500 * 2 ** attempt)));&lt;br&gt;
  }&lt;br&gt;
  throw new Error("ERP unavailable after 5 attempts");&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Why a key table instead of trusting the ERP alone? Many ERP APIs do not enforce uniqueness on an external reference. If yours does not, look the order up by that field before creating it. Why not broker-level exactly-once delivery? It covers the queue, not the HTTP call to the ERP. The table costs one extra write per event, the price of a guard you control.&lt;/p&gt;

&lt;h1&gt;
  
  
  Real-World Application
&lt;/h1&gt;

&lt;p&gt;In one of our ERP integration projects at &lt;a href="//oodles.com"&gt;Oodles&lt;/a&gt;, we implemented an open-source ERP for a Shanghai-based logistics provider, covering inventory management, order processing, and real-time tracking. &lt;/p&gt;

&lt;p&gt;Derive keys from the business event and entity version, never from the request attempt.&lt;/p&gt;

&lt;p&gt;Claim keys with ON CONFLICT, because check-then-insert races under concurrent webhooks.&lt;/p&gt;

&lt;p&gt;Retry only 5xx and 429 responses, and fail fast on every other 4xx.&lt;br&gt;
Apply full jitter so workers do not hit a recovering ERP in lockstep.&lt;/p&gt;

&lt;p&gt;Hit another duplicate-write edge case? Share it in the comments, or talk to us about &lt;a href="//oodles.com/contact-us"&gt;ERP integration services&lt;/a&gt; for a design review.&lt;/p&gt;

&lt;h1&gt;
  
  
  What are ERP integration services?
&lt;/h1&gt;

&lt;p&gt;ERP integration services connect an ERP to other systems, such as storefronts, CRMs, and warehouse tools, through APIs, middleware, or files. They cover data mapping, sync logic, error handling, and monitoring so records stay consistent across systems.&lt;/p&gt;

&lt;h1&gt;
  
  
  Why do duplicate records appear in ERP integrations?
&lt;/h1&gt;

&lt;p&gt;In ERP integration services Most webhook senders and queues guarantee at-least-once delivery. If a handler creates a record before acknowledging, any timeout triggers a retry that creates a second record. Without an idempotency key, the ERP cannot tell them apart.&lt;/p&gt;

&lt;h1&gt;
  
  
  What is an idempotency key?
&lt;/h1&gt;

&lt;p&gt;An idempotency key is a unique value, derived from the business event, sent with each write. The receiver stores it and returns the original result for repeats, so retries cannot create duplicate records.&lt;/p&gt;

&lt;h1&gt;
  
  
  Should I retry 4xx errors from an ERP API?
&lt;/h1&gt;

&lt;p&gt;No, except 429. A 4xx response usually signals invalid data or missing permissions, which retries cannot fix. Retry 5xx and 429 with exponential backoff and jitter, and send persistent failures to a dead-letter queue.&lt;/p&gt;

&lt;h1&gt;
  
  
  How long should idempotency keys be stored?
&lt;/h1&gt;

&lt;p&gt;In ERP integration services Store keys longer than your maximum retry window and any sender replay window. Then purge older rows on a schedule, and keep the ERP-assigned ID elsewhere if audits need it.&lt;/p&gt;

</description>
      <category>erp</category>
      <category>node</category>
      <category>api</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Optimizing OptaPlanner: 5 Variables to Benchmark Before Production</title>
      <dc:creator>Sanya Mittal</dc:creator>
      <pubDate>Tue, 27 Oct 2026 07:33:58 +0000</pubDate>
      <link>https://dev.to/sanya_mittal_a509a2c50a2d/optimizing-optaplanner-5-variables-to-benchmark-before-production-3mnk</link>
      <guid>https://dev.to/sanya_mittal_a509a2c50a2d/optimizing-optaplanner-5-variables-to-benchmark-before-production-3mnk</guid>
      <description>&lt;p&gt;A scheduling solver can return a valid result and still perform poorly in production. It might leave too many soft constraints unsatisfied, take longer than the planning window allows, or produce inconsistent results as the workload grows.&lt;/p&gt;

&lt;p&gt;It helps solve resource-allocation problems such as employee rostering, vehicle routing, and production scheduling. The challenge is choosing a solver configuration that works for your actual workload, not just a small development dataset.&lt;/p&gt;

&lt;p&gt;Before deploying a scheduling engine, benchmark these five variables using representative inputs. This is especially important when planning decisions must meet operational deadlines. Teams evaluating &lt;a href="https://www.oodles.com/planning-solutions-/optaplanner/how-optaplanner-transforms-complex-scheduling-into-seamless-operations?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=backlink&amp;amp;utm_content=devto_article_39" rel="noopener noreferrer"&gt;OptaPlanner scheduling solutions&lt;/a&gt; should measure solution quality and execution cost together, rather than treating speed as the only success criterion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context and Setup
&lt;/h2&gt;

&lt;p&gt;It uses planning entities, variables, and constraints to model a problem and search for better solutions. Hard constraints represent rules that must not be broken, while soft constraints express preferences that should be optimized.&lt;/p&gt;

&lt;p&gt;For example, a factory scheduler might need to assign jobs to machines while respecting machine capacity, delivery deadlines, setup requirements, and operator availability.&lt;/p&gt;

&lt;p&gt;OptaPlanner's official documentation, under "Score calculation performance tricks," notes that score calculation speed is normally above 1,000 calculations per second for many high-scale datasets, except when using an &lt;code&gt;EasyScoreCalculator&lt;/code&gt;. This is a useful diagnostic reference, not a universal performance guarantee. Dataset complexity and constraint design influence the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  OptaPlanner: 5 Variables to Benchmark
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Measure solution quality
&lt;/h3&gt;

&lt;p&gt;Start by measuring whether the solver produces operationally acceptable schedules.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Record the final score and its hard and soft components.&lt;/li&gt;
&lt;li&gt;Check hard-constraint violations independently.&lt;/li&gt;
&lt;li&gt;Compare soft-constraint penalties against an agreed baseline.&lt;/li&gt;
&lt;li&gt;Repeat the test with realistic and difficult datasets.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A higher score is useful only when the score definition accurately represents business priorities. A missing constraint can make an apparently excellent solution operationally invalid.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Measure score-calculation speed
&lt;/h3&gt;

&lt;p&gt;Score calculation is performed repeatedly during the search. Inefficient joins, unnecessary matching, and expensive custom calculations can consume time that would otherwise be spent exploring better solutions.&lt;/p&gt;

&lt;p&gt;For example, a Constraint Streams rule can penalize overlapping assignments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Constraint&lt;/span&gt; &lt;span class="nf"&gt;overlappingShifts&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;ConstraintFactory&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forEach&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Shift&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;join&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Shift&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;Joiners&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Shift:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getEmployee&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
            &lt;span class="nc"&gt;Joiners&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lessThan&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Shift:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getStart&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getEnd&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;isAfter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStart&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;penalize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HardSoftScore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ONE_HARD&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;asConstraint&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Overlapping shifts"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Why: penalizes overlapping assignments for one employee.&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example assumes that &lt;code&gt;Shift&lt;/code&gt; exposes the shown properties and that both shifts have assigned employees. Production code should also account for unassigned shifts and ensure each overlapping pair is counted only once. The exact temporal model should match the application's scheduling rules.&lt;/p&gt;

&lt;p&gt;Measure calculation speed using the solver's logs and compare equivalent datasets. Do not optimize this metric in isolation: faster scoring does not automatically produce a better final schedule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Increase dataset size gradually
&lt;/h3&gt;

&lt;p&gt;A solver configuration that performs well with 50 jobs may behave differently with 500 or 5,000 jobs.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create small, medium, and large representative datasets.&lt;/li&gt;
&lt;li&gt;Keep constraint definitions and test conditions consistent.&lt;/li&gt;
&lt;li&gt;Record score quality and solve duration at each scale.&lt;/li&gt;
&lt;li&gt;Investigate nonlinear growth before increasing production capacity.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Include difficult cases, not just larger copies of an easy dataset. Tight deadlines, scarce resources, and conflicting requirements can change the search difficulty substantially.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Tune the solve-time limit
&lt;/h3&gt;

&lt;p&gt;Set the solver's termination limit according to the business deadline. A warehouse schedule needed within seconds has different requirements from a production plan that can run overnight.&lt;/p&gt;

&lt;p&gt;Use OptaPlanner's benchmarking facilities to compare configurations against the same input scenarios. Evaluate both the best score reached within the time limit and how quickly the solver improves that score.&lt;/p&gt;

&lt;p&gt;A longer run may improve solution quality, but the improvement must justify the extra compute time. Establish an acceptance threshold before selecting a configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Check consistency across runs
&lt;/h3&gt;

&lt;p&gt;Heuristics can produce different schedules across runs, depending on configuration, randomization, and search behavior.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run each candidate configuration several times.&lt;/li&gt;
&lt;li&gt;Record score, duration, and hard-constraint validity.&lt;/li&gt;
&lt;li&gt;Compare median results and worst observed outcomes.&lt;/li&gt;
&lt;li&gt;Document the random seed and termination settings.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is important when operations teams expect predictable planning quality. A configuration with a strong best-case result but unreliable typical performance may be a poor production choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Application
&lt;/h2&gt;

&lt;p&gt;For an Oodles scheduling project, the useful case study is one that connects solver tuning to an operational result. The project evidence should identify the scheduling system, its bottleneck, the optimization approach, and a measured outcome.&lt;/p&gt;

&lt;p&gt;A publishable case study needs verified details such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;System:&lt;/strong&gt; &lt;code&gt;[Verified scheduling use case]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Problem:&lt;/strong&gt; &lt;code&gt;[Measured baseline bottleneck]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approach:&lt;/strong&gt; Benchmarking, constraint analysis, and configuration tuning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result:&lt;/strong&gt; &lt;code&gt;[Verified change in solve time or solution quality]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conditions:&lt;/strong&gt; Dataset size, hardware, solver configuration, and measurement period.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These metrics should come from actual project records before publication. Oodles' broader engineering work is described on the &lt;a href="https://www.oodles.com/" rel="noopener noreferrer"&gt;Oodles&lt;/a&gt; website.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Measure solution quality and execution time together.&lt;/li&gt;
&lt;li&gt;Validate hard constraints before comparing soft scores.&lt;/li&gt;
&lt;li&gt;Benchmark with realistic datasets at multiple scales.&lt;/li&gt;
&lt;li&gt;Tune termination limits against operational deadlines.&lt;/li&gt;
&lt;li&gt;Repeat tests to understand consistency and worst-case behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What is OptaPlanner used for?
&lt;/h3&gt;

&lt;p&gt;It is a Java constraint-solving engine used for planning problems such as employee scheduling, vehicle routing, production planning, and timetabling. It searches for solutions that satisfy hard constraints while optimizing soft constraints.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I improve OptaPlanner performance?
&lt;/h3&gt;

&lt;p&gt;Start by profiling score calculation, reviewing expensive Constraint Streams operations, and testing representative datasets. Then compare solver configurations under consistent termination limits. Measure solution quality as well as execution speed before adopting a change.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is a hard constraint in OptaPlanner?
&lt;/h3&gt;

&lt;p&gt;A hard constraint represents a rule that a valid solution must satisfy, such as preventing one employee from working overlapping shifts. A soft constraint represents a preference, such as reducing undesirable shift assignments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why should I benchmark different solver configurations?
&lt;/h3&gt;

&lt;p&gt;Different configurations can behave differently across problem sizes and constraint patterns. Benchmarking helps determine which configuration provides the best balance between solution quality, solve duration, and consistency for your workload.&lt;/p&gt;

&lt;h3&gt;
  
  
  When should I review an OptaPlanner implementation?
&lt;/h3&gt;

&lt;p&gt;Review it when planning duration increases, workloads grow, business rules change, or solution quality falls below operational expectations. Teams can discuss an &lt;a href="https://www.oodles.com/contact-us" rel="noopener noreferrer"&gt;OptaPlanner&lt;/a&gt; implementation with Oodles when they need help assessing these issues.&lt;/p&gt;

</description>
      <category>timefold</category>
      <category>shiftplanning</category>
      <category>fleet</category>
      <category>ai</category>
    </item>
    <item>
      <title>ERP Consulting Services: Test Partial Receipts in AL</title>
      <dc:creator>Sanya Mittal</dc:creator>
      <pubDate>Mon, 19 Oct 2026 09:16:02 +0000</pubDate>
      <link>https://dev.to/sanya_mittal_a509a2c50a2d/erp-consulting-services-test-partial-receipts-in-al-3c5b</link>
      <guid>https://dev.to/sanya_mittal_a509a2c50a2d/erp-consulting-services-test-partial-receipts-in-al-3c5b</guid>
      <description>&lt;p&gt;Partial receipts are a small business rule with consequences across purchasing, inventory, and accounts payable. In Microsoft Dynamics 365 Business Central, a quantity-validation mistake can allow an extension to calculate the wrong remaining quantity or reject a legitimate receipt. &lt;a href="https://www.oodles.com/custom-erp/11/solutions-explainer?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=backlink&amp;amp;utm_content=devto_article_35" rel="noopener noreferrer"&gt;ERP consulting services&lt;/a&gt; need to define that behavior before developers implement it. In this walkthrough, we will encode a simple receipt rule in AL and test its expected outcomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;ERP consulting services for Microsoft Dynamics 365 Business Central development sandbox.&lt;/li&gt;
&lt;li&gt;Visual Studio Code with the Microsoft AL Language extension.&lt;/li&gt;
&lt;li&gt;An AL project targeting Business Central runtime &lt;code&gt;15.0&lt;/code&gt; or later.&lt;/li&gt;
&lt;li&gt;An available object ID range for your extension.&lt;/li&gt;
&lt;li&gt;A test environment where you can compile and execute test codeunits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The example uses core AL language features and does not require an external library. Microsoft documents the test-codeunit model in its &lt;a href="https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-test-codeunits-and-test-methods" rel="noopener noreferrer"&gt;official AL testing guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the obvious approach fails
&lt;/h2&gt;

&lt;p&gt;A common mistake is to treat receipt validation as a single subtraction operation. The arithmetic is correct, but the business rule is incomplete.&lt;/p&gt;

&lt;p&gt;Consider this fragment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RemainingQty := OrderedQty - ReceivedQty;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a fragment, not a complete AL object. It assumes both quantities are valid and that the receipt cannot exceed the order quantity.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;OrderedQty&lt;/code&gt; is &lt;code&gt;10&lt;/code&gt; and &lt;code&gt;ReceivedQty&lt;/code&gt; is &lt;code&gt;12&lt;/code&gt;, the result is &lt;code&gt;-2&lt;/code&gt;. The calculation has no knowledge of whether that outcome is permitted by the business process.&lt;/p&gt;

&lt;p&gt;The opposite mistake is rejecting every receipt that differs from the ordered quantity. That prevents legitimate partial deliveries.&lt;/p&gt;

&lt;p&gt;The first consulting decision is therefore not which function to write. It is defining the allowed states: zero receipt, partial receipt, full receipt, negative quantities, and over-receipt. Those states become our acceptance criteria.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Put the quantity rule in one codeunit
&lt;/h2&gt;

&lt;p&gt;We will separate the calculation from the page or posting workflow. This keeps the rule independently testable and prevents multiple UI entry points from implementing different versions.&lt;/p&gt;

&lt;p&gt;Create a file named &lt;code&gt;ReceiptQtyRules.Codeunit.al&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;codeunit 50100 "Receipt Qty Rules"
{
    procedure GetRemainingQty(
        OrderedQty: Decimal;
        ReceivedQty: Decimal
    ): Decimal
    begin
        if OrderedQty &amp;lt; 0 then
            Error('Ordered quantity cannot be negative.');

        if ReceivedQty &amp;lt; 0 then
            Error('Received quantity cannot be negative.');

        if ReceivedQty &amp;gt; OrderedQty then
            Error('Received quantity cannot exceed ordered quantity.');

        exit(OrderedQty - ReceivedQty);
    end;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This codeunit defines a specific policy: receipts can be partial, but negative quantities and quantities above the ordered amount are invalid. Confirm that policy with the business owner before connecting it to an actual posting process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Turn the business rules into automated tests
&lt;/h2&gt;

&lt;p&gt;A successful compilation proves that the code is syntactically valid. It does not prove that the rule handles the business cases correctly.&lt;/p&gt;

&lt;p&gt;Create a separate test codeunit named &lt;code&gt;ReceiptQtyRulesTests.Codeunit.al&lt;/code&gt;. The tests below cover a partial receipt, a complete receipt, and an untouched order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;codeunit 50101 "Receipt Qty Rules Tests"
{
    Subtype = Test;

    [Test]
    procedure PartialReceiptReturnsRemainingQty()
    var
        ReceiptRules: Codeunit "Receipt Qty Rules";
    begin
        if ReceiptRules.GetRemainingQty(10, 4) &amp;lt;&amp;gt; 6 then
            Error('Partial receipt returned an incorrect quantity.');
    end;

    [Test]
    procedure FullReceiptReturnsZero()
    var
        ReceiptRules: Codeunit "Receipt Qty Rules";
    begin
        if ReceiptRules.GetRemainingQty(10, 10) &amp;lt;&amp;gt; 0 then
            Error('Full receipt should leave zero quantity.');
    end;

    [Test]
    procedure ZeroReceiptReturnsOrderedQty()
    var
        ReceiptRules: Codeunit "Receipt Qty Rules";
    begin
        if ReceiptRules.GetRemainingQty(10, 0) &amp;lt;&amp;gt; 10 then
            Error('Zero receipt should leave ordered quantity.');
    end;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each test fails when its expected result does not match the returned value. The tests exercise the business rule directly, without requiring a purchase order page or a warehouse workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Verify the rejection paths
&lt;/h2&gt;

&lt;p&gt;The first three tests cover valid quantities. We must also verify that invalid inputs trigger errors rather than silently returning negative balances.&lt;/p&gt;

&lt;p&gt;In ERP consulting services Business Central test codeunits support error assertions through the test framework. For this small example, you can add a dedicated error-handling test using the appropriate assertion codeunit available in your project's test dependencies. Confirm the exact assertion API and dependency version before adding it.&lt;/p&gt;

&lt;p&gt;At minimum, manually verify these cases in the development sandbox:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ordered&lt;/th&gt;
&lt;th&gt;Received&lt;/th&gt;
&lt;th&gt;Expected behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Remaining quantity is 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Remaining quantity is 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Remaining quantity is 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Reject the receipt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;-1&lt;/td&gt;
&lt;td&gt;Reject the receipt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Reject the order quantity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The table is the acceptance matrix, not a substitute for automated negative-path tests. The over-receipt rule may differ when a business explicitly permits over-delivery within a defined tolerance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Compile and execute the tests
&lt;/h2&gt;

&lt;p&gt;Compile the extension in Visual Studio Code using the AL development workflow. Then run the test codeunit in a Business Central sandbox with the test runner and test dependencies configured.&lt;/p&gt;

&lt;p&gt;The expected result is three passing tests. If a test fails, inspect the actual quantity and the error message before changing the implementation. Do not weaken an assertion to make a build pass without checking the intended business rule.&lt;/p&gt;

&lt;p&gt;For continuous integration, compile the extension and execute its test suite before promoting the package to another environment. The exact command depends on the installed AL tooling and Business Central environment. Pin the tool version in your pipeline rather than assuming every developer uses the same release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs and limitations
&lt;/h2&gt;

&lt;p&gt;The example is intentionally small. It isolates one quantity rule, but it does not implement a complete purchasing or warehouse process.&lt;/p&gt;

&lt;p&gt;First, a receipt quantity is not always the same as the quantity invoiced or the quantity physically available. Business Central's posting workflow, unit-of-measure conversions, and warehouse handling may impose additional rules. A helper function cannot replace validation against those records.&lt;/p&gt;

&lt;p&gt;Second, some businesses allow over-receipts within a tolerance. The example rejects every quantity above the ordered amount, so that policy would require an explicit tolerance rule and additional tests.&lt;/p&gt;

&lt;p&gt;Third, the example does not prevent concurrent processes from changing the underlying order. A real posting operation must use the platform's established transaction and validation behavior rather than trusting a previously calculated remaining quantity.&lt;/p&gt;

&lt;p&gt;Finally, a passing unit test does not prove that a complete purchase receipt posts correctly. Integration tests must verify the relevant records, posting behavior, inventory effects, and financial consequences.&lt;/p&gt;

&lt;p&gt;At &lt;a href="//oodles.com"&gt;Oodles&lt;/a&gt;, we connect business requirements with practical software engineering decisions to help teams define, build, and test ERP extensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-world application: From consulting scope to testable AL rules
&lt;/h2&gt;

&lt;p&gt;We implemented this principle in a Business Central project for a logistics client, where the agreed delivery scope included approximately 62 AL extensions over an 18-week plan.&lt;/p&gt;

&lt;p&gt;The delivery plan used two-week sprints with separate development, quality assurance, and project management responsibilities. That scope required more than listing extensions. Each extension needed a defined purpose, an agreed boundary, and a testable outcome.&lt;/p&gt;

&lt;p&gt;For a receipt-related requirement, that means documenting what happens when a shipment arrives partially, exceeds the expected quantity, or conflicts with the purchase order. The acceptance criteria must distinguish valid exceptions from errors before development begins.&lt;/p&gt;

&lt;p&gt;The ERP consulting services documented project scope provides two verified planning figures: approximately 62 extensions and 18 weeks. We do not have a verified defect reduction, processing-time improvement, or cost saving to report.&lt;/p&gt;

&lt;p&gt;This is where ERP consulting services connect business analysis to engineering. A clear requirement gives developers a rule to implement and testers a condition to verify. It also gives stakeholders a basis for deciding whether a requested behavior belongs in the original scope or requires an approved change.&lt;/p&gt;

&lt;p&gt;Our &lt;a href="https://www.oodles.com/contact-us" rel="noopener noreferrer"&gt;ERP Consulting Services&lt;/a&gt; explains the broader context of building ERP solutions around business requirements. The same discipline applies whether an extension changes purchasing, inventory, or financial workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Define valid business states before implementing quantity calculations.&lt;/li&gt;
&lt;li&gt;Keep reusable business rules separate from page and posting workflows.&lt;/li&gt;
&lt;li&gt;Test partial, complete, zero, and invalid receipt quantities.&lt;/li&gt;
&lt;li&gt;Confirm over-receipt policy with the business owner before enforcing a hard limit.&lt;/li&gt;
&lt;li&gt;Treat unit tests as one layer of validation, not proof of end-to-end posting correctness.&lt;/li&gt;
&lt;li&gt;Require measurable acceptance criteria for every material ERP extension.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have encountered an AL testing edge case around partial receipts, posting validation, or warehouse quantities, share the scenario and expected behavior. Comparing those cases is a useful way to improve acceptance criteria before the next release.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should partial receipt validation run on the page or in a codeunit?
&lt;/h3&gt;

&lt;p&gt;A reusable business rule belongs in a testable codeunit or an appropriate existing business-logic component. Page-level validation can improve user feedback, but it should not be the only protection for a rule that must hold across multiple entry points.&lt;/p&gt;

&lt;h3&gt;
  
  
  In ERP Consulting Services Can Business Central allow receipt quantities above the ordered amount?
&lt;/h3&gt;

&lt;p&gt;ERP consulting services that depends on the configured process and the applicable business rules. If over-receipts are permitted, define the tolerance and test the boundary values. Do not copy the strict rejection rule into a process that explicitly permits controlled over-delivery.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does a passing AL unit test not guarantee successful posting?
&lt;/h3&gt;

&lt;p&gt;A unit test verifies the specific logic it executes. Actual posting may involve inventory entries, warehouse records, permissions, and other validations. Use integration tests in a sandbox to verify those interactions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should receipt rules be duplicated across extensions?
&lt;/h3&gt;

&lt;p&gt;Avoid duplicating the same business rule when a shared component or standard Business Central behavior can enforce it appropriately. However, reuse must respect the application's extension boundaries and posting events. Review the actual dependencies before extracting a common component.&lt;/p&gt;

&lt;h3&gt;
  
  
  In ERP Consulting Services How should developers test an ERP extension after a platform update?
&lt;/h3&gt;

&lt;p&gt;Compile against the target platform's supported symbols, rerun automated tests, and exercise critical business workflows in a sandbox. Also review changed APIs and dependencies. Pin tooling and runtime versions so the test results are reproducible.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>erp</category>
    </item>
    <item>
      <title>How to Build an Inventory Management Solution That Prevents Stock Drift</title>
      <dc:creator>Sanya Mittal</dc:creator>
      <pubDate>Thu, 15 Oct 2026 05:21:43 +0000</pubDate>
      <link>https://dev.to/sanya_mittal_a509a2c50a2d/how-to-build-an-inventory-management-solution-that-prevents-stock-drift-9m4</link>
      <guid>https://dev.to/sanya_mittal_a509a2c50a2d/how-to-build-an-inventory-management-solution-that-prevents-stock-drift-9m4</guid>
      <description>&lt;p&gt;A warehouse dashboard can show 120 units available while the picking team finds only 93. The discrepancy may come from delayed order reservations, duplicate webhook events, unrecorded returns, or concurrent updates from multiple sales channels. The result is overselling, failed fulfilment, and replenishment decisions based on incorrect data.&lt;/p&gt;

&lt;p&gt;An Inventory Management Solution needs to do more than store stock quantities. It must preserve consistency across receiving, picking, order allocation, returns, and stock adjustments, even when multiple services update inventory simultaneously.&lt;/p&gt;

&lt;p&gt;The architectural challenge is deciding which component owns the stock balance, how transactions are applied, and how failed integrations recover without creating duplicate movements. In this guide, we will design a practical inventory workflow using Node.js, PostgreSQL, and asynchronous messaging. For broader warehouse architecture considerations, explore &lt;a href="https://www.oodles.com/inventory-warehouse-management-/2172960/solutions-explainer?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=backlink&amp;amp;utm_content=devto_article_32" rel="noopener noreferrer"&gt;inventory and warehouse management solutions&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context and Setup
&lt;/h2&gt;

&lt;p&gt;An Inventory Management Solution typically receives updates from e-commerce applications, ERP systems, warehouse management software, and third-party logistics providers. Each system may report a different event, but the inventory service must maintain one consistent view of available stock.&lt;/p&gt;

&lt;p&gt;Consider a SKU with 100 units on hand and 15 units already reserved for unfulfilled orders.&lt;/p&gt;

&lt;p&gt;The system should calculate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On-hand quantity:&lt;/strong&gt; 100 units physically recorded in stock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reserved quantity:&lt;/strong&gt; 15 units allocated to existing orders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Available quantity:&lt;/strong&gt; 85 units eligible for new allocations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A common implementation mistake is storing only a single mutable quantity. Without reservation tracking and transaction history, it becomes difficult to explain why stock changed or safely reverse an operation.&lt;/p&gt;

&lt;p&gt;Research published in &lt;em&gt;Management Science&lt;/em&gt; by DeHoratius and Raman in 2008 examined nearly 370,000 inventory records across 37 stores of one retailer. The researchers found that 65% of the records were inaccurate. This historical study demonstrates why inventory accuracy must be supported by operational controls rather than assumed from database values alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;The example assumes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Node.js with an asynchronous PostgreSQL driver.&lt;/li&gt;
&lt;li&gt;PostgreSQL tables for stock balances, reservations, and inventory movements.&lt;/li&gt;
&lt;li&gt;A stable order identifier for deduplicating incoming events.&lt;/li&gt;
&lt;li&gt;Database transactions and row-level locking.&lt;/li&gt;
&lt;li&gt;A background worker for retrying failed downstream notifications.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Designing an Inventory Management Solution With Transaction Safety
&lt;/h2&gt;

&lt;p&gt;The central design principle is to treat inventory changes as transactions, not independent quantity updates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Define the Inventory Data Model
&lt;/h3&gt;

&lt;p&gt;Store physical stock, reservations, and movement history separately. This provides an auditable record of receiving, dispatches, returns, and adjustments.&lt;/p&gt;

&lt;p&gt;A simplified schema could contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;inventory_balance&lt;/code&gt;: SKU, warehouse, on-hand quantity, reserved quantity, version.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;inventory_reservation&lt;/code&gt;: order ID, SKU, quantity, reservation status.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;inventory_movement&lt;/code&gt;: movement ID, SKU, quantity delta, reason, reference ID, timestamp.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;processed_events&lt;/code&gt;: event ID and processing status.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use a composite key on &lt;code&gt;(sku, warehouse_id)&lt;/code&gt; to prevent multiple balance records for the same product and location.&lt;/p&gt;

&lt;p&gt;The movement table should be append-only for normal operations. Corrections should create compensating movements instead of silently rewriting historical transactions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Apply Stock Changes Atomically
&lt;/h3&gt;

&lt;p&gt;A stock check followed by an independent update creates a race condition. Two checkout requests could both read the same available quantity and reserve more stock than exists.&lt;/p&gt;

&lt;p&gt;The following PostgreSQL statement performs a conditional reservation. It assumes &lt;code&gt;inventory_balance&lt;/code&gt; contains the SKU's current on-hand and reserved quantities.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Why: update only when sufficient unreserved stock exists.&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;inventory_balance&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;reserved_qty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reserved_qty&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;warehouse_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;on_hand_qty&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;reserved_qty&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on_hand_qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reserved_qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- No returned row means the reservation was rejected.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Execute this statement inside a database transaction alongside reservation creation, event deduplication, and movement recording. Commit all changes together or roll them back.&lt;/p&gt;

&lt;p&gt;The application must also validate that the requested quantity is positive and that the event has not already been processed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Make Event Processing Idempotent
&lt;/h3&gt;

&lt;p&gt;Payment confirmations, order cancellations, and shipping notifications can be delivered more than once. An idempotent consumer ensures that replaying the same event does not apply the same inventory movement twice.&lt;/p&gt;

&lt;p&gt;Use a unique constraint on the event identifier and insert the deduplication record within the same database transaction as the stock update.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reserveInventory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BEGIN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Why: a unique event ID prevents duplicate processing.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`INSERT INTO processed_events (event_id)
       VALUES ($1)
       ON CONFLICT (event_id) DO NOTHING
       RETURNING event_id`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rowCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;duplicate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Why: the conditional update prevents overselling.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`UPDATE inventory_balance
       SET reserved_qty = reserved_qty + $1,
           version = version + 1
       WHERE sku = $2
         AND warehouse_id = $3
         AND on_hand_qty - reserved_qty &amp;gt;= $1
       RETURNING sku`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;warehouseId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rowCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Insufficient stock&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Persist the reservation and movement here,&lt;/span&gt;
    &lt;span class="c1"&gt;// using the same transaction and client.&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;COMMIT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;reserved&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Let the caller retry or handle rejection.&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a simplified reservation example, not a complete production service. Production code must validate payloads, handle database connection failures, distinguish permanent business rejections from transient errors, and ensure reservation and movement records are committed in the same transaction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Separate Database Commits From External Notifications
&lt;/h3&gt;

&lt;p&gt;Do not call a shipping API or publish a message while holding a database transaction open. External requests can time out after the remote service has already processed them.&lt;/p&gt;

&lt;p&gt;Instead, use the transactional outbox pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Save the inventory changes and an outbox event in one database transaction.&lt;/li&gt;
&lt;li&gt;Let a background worker publish committed outbox events.&lt;/li&gt;
&lt;li&gt;Retry transient failures with exponential backoff.&lt;/li&gt;
&lt;li&gt;Deduplicate messages at the receiving service.&lt;/li&gt;
&lt;li&gt;Monitor outbox age, retry counts, and dead-letter events.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A direct message publish after a database commit is simpler, but a process crash between those operations can lose the notification. The outbox adds storage and worker complexity while making recovery more reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Application
&lt;/h2&gt;

&lt;p&gt;At Oodles, an inventory implementation should begin by identifying where stock is created, reserved, consumed, and reconciled across connected systems. The appropriate design depends on the existing ERP, warehouse workflows, transaction volume, and integration contracts.&lt;/p&gt;

&lt;p&gt;For example, consider a multi-channel retail workflow in which orders arrive through an online store while warehouse staff receive and dispatch stock through a separate system. A suitable implementation would centralize inventory movements, use transactional reservations, deduplicate inbound events, and reconcile warehouse quantities against system balances.&lt;/p&gt;

&lt;p&gt;Measure the outcome using baseline and post-deployment data rather than assuming a performance improvement. Useful indicators include reservation failure rate, duplicate movement count, inventory record accuracy, API latency at the 95th percentile, and the time required to recover failed messages.&lt;/p&gt;

&lt;p&gt;No verified Oodles project metrics were supplied for this article, so specific before-and-after improvements should be reported only after they have been measured in an actual deployment. You can explore &lt;a href="https://oodles.com/" rel="noopener noreferrer"&gt;Oodles&lt;/a&gt; for more information about its Inventory Management Solution software engineering and integration services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Separate stock concepts:&lt;/strong&gt; Track on-hand, reserved, and available quantities independently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforce consistency in the database:&lt;/strong&gt; Use conditional updates, transactions, and appropriate constraints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design for duplicate delivery:&lt;/strong&gt; Make event processing idempotent using stable event identifiers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recover from integration failures:&lt;/strong&gt; Use a transactional outbox, retries, and dead-letter handling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure operational correctness:&lt;/strong&gt; Track inventory discrepancies, reservation failures, and recovery times.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  1. What is an Inventory Management Solution?
&lt;/h3&gt;

&lt;p&gt;An Inventory Management Solution is software that tracks stock quantities, reservations, receipts, transfers, dispatches, and adjustments across warehouses or sales channels. It helps businesses maintain accurate stock records and coordinate replenishment and order fulfilment.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. How do you prevent overselling in an inventory system?
&lt;/h3&gt;

&lt;p&gt;Inventory Management Solution Prevent overselling by checking available stock and updating reservations atomically within a database transaction. Use conditional SQL updates or row-level locking, enforce quantity constraints, and ensure concurrent requests cannot reserve more units than are available.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Should inventory updates use synchronous APIs or message queues?
&lt;/h3&gt;

&lt;p&gt;Synchronous APIs are useful when an operation requires an immediate reservation decision. Message queues are suitable for downstream notifications, warehouse events, and asynchronous integrations. A transactional outbox can connect committed database changes to reliable event publication.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. How does an Inventory Management Solution handle duplicate webhooks?
&lt;/h3&gt;

&lt;p&gt;It stores a stable event identifier under a unique database constraint and processes that event within the same transaction as the inventory changes. If the event has already been committed, the consumer skips the duplicate instead of applying the stock movement again.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Which metrics should developers monitor in inventory software?
&lt;/h3&gt;

&lt;p&gt;Developers should monitor inventory record accuracy, reservation rejection rates, duplicate event counts, API latency, outbox processing delays, and failed-message retries. These metrics reveal whether the system maintains correct stock balances and recovers effectively from integration failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Discuss Your Inventory Architecture
&lt;/h2&gt;

&lt;p&gt;How does your current system handle concurrent reservations, duplicate order events, or inventory reconciliation across warehouses? Share your implementation challenges and questions in the comments.&lt;/p&gt;

&lt;p&gt;If you are planning an &lt;a href="https://oodles.com/contact-us" rel="noopener noreferrer"&gt;Inventory Management Solution&lt;/a&gt;, discuss your requirements with the Oodles team, including your existing ERP, warehouse workflows, integration points, and consistency requirements.&lt;/p&gt;

</description>
      <category>inventory</category>
      <category>webdev</category>
      <category>enterprisesystem</category>
      <category>opensource</category>
    </item>
    <item>
      <title>QuickBooks Implementation Services: Fix Webhook Retries</title>
      <dc:creator>Sanya Mittal</dc:creator>
      <pubDate>Mon, 12 Oct 2026 10:29:12 +0000</pubDate>
      <link>https://dev.to/sanya_mittal_a509a2c50a2d/quickbooks-implementation-services-fix-webhook-retries-27lp</link>
      <guid>https://dev.to/sanya_mittal_a509a2c50a2d/quickbooks-implementation-services-fix-webhook-retries-27lp</guid>
      <description>&lt;p&gt;&lt;a href="https://erpsolutions.oodles.io/quickbooks-erp-integration-services/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=backlink&amp;amp;utm_content=devto_article_31" rel="noopener noreferrer"&gt;Quickbooks Implementation Services&lt;/a&gt; involve more than connecting an accounting application to another system. A QuickBooks Online webhook can arrive twice, arrive late, or never reach your application. If your integration treats every notification as a new accounting command, retries can trigger duplicate work or leave financial records stale.&lt;/p&gt;

&lt;p&gt;For developers building accounting integrations, the difficult part is recovering the correct entity state when notifications are duplicated, missed, or delivered out of order. In this walkthrough, we will build a retry-safe reconciliation worker using Node.js and examine where the approach breaks.&lt;/p&gt;

&lt;h1&gt;
  
  
  Prerequisites
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 22.12.0&lt;/li&gt;
&lt;li&gt;A QuickBooks Online developer application and sandbox company for live integration testing&lt;/li&gt;
&lt;li&gt;Familiarity with webhooks, asynchronous processing, and JSON&lt;/li&gt;
&lt;li&gt;A persistent queue and database for production deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Why QuickBooks Webhook Handlers Fail
&lt;/h1&gt;

&lt;p&gt;Quickbooks Implementation Services often require integrations to handle repeated notifications safely. A basic webhook handler receives a notification and immediately updates a local record. This assumes delivery occurs once and follows chronological order.&lt;/p&gt;

&lt;p&gt;Intuit documents webhook retries when notifications are not acknowledged successfully. Its webhook guidance also recommends asynchronous processing because the endpoint has limited time to respond.&lt;/p&gt;

&lt;p&gt;Consider this failure sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10:00:00  Invoice notification received
10:00:03  Handler times out before acknowledgement
10:00:10  QuickBooks retries the notification
10:00:11  Both workers process the same invoice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A database uniqueness constraint may prevent duplicate records, but it cannot prevent repeated emails, exports, or external side effects. Reliable accounting synchronization needs both duplicate protection and a recovery mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Acknowledge Webhooks Quickly
&lt;/h2&gt;

&lt;p&gt;Quickbooks Implementation Services should separate notification delivery from expensive reconciliation work. Verify the incoming notification, persist the required work, and acknowledge it without waiting for several downstream API calls.&lt;/p&gt;

&lt;p&gt;Use a durable queue rather than relying on an in-memory task. This lets workers resume processing after a restart and reduces the chance of losing accepted work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Coalesce Duplicate Notifications
&lt;/h2&gt;

&lt;p&gt;Quickbooks Implementation Services benefit from entity-level coalescing when the application needs the latest accounting state rather than every intermediate event. Notifications for the same company and entity can share one pending refresh.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;reconcile.mjs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;realmId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastUpdated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastUpdated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reconcile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`Refresh &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;flushQueue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()];&lt;/span&gt;
  &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;notification&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;reconcile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;realmId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sandbox-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invoice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;lastUpdated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-10-12T10:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;realmId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sandbox-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invoice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;lastUpdated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-10-12T10:01:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;flushQueue&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the example with &lt;code&gt;node reconcile.mjs&lt;/code&gt;. Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Refresh Invoice 42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both notifications produce one refresh. This example demonstrates coalescing, not durable processing, because the map disappears when the process exits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Fetch the Latest Accounting State
&lt;/h2&gt;

&lt;p&gt;Quickbooks Implementation Services should treat webhook notifications as change signals, not as a complete and ordered ledger of entity changes. The reconciliation worker should retrieve the current entity through the supported QuickBooks Online API.&lt;/p&gt;

&lt;p&gt;For updates, Quickbooks Implementation Services entities expose a &lt;code&gt;SyncToken&lt;/code&gt; used for optimistic concurrency. Before updating an entity, retrieve its current state and use the appropriate token. &lt;/p&gt;

&lt;p&gt;Your integration must also handle deleted records, unsupported entity operations, and conflicts. Do not assume every notification represents an ordinary update.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Recover Missed Changes with CDC
&lt;/h2&gt;

&lt;p&gt;Quickbooks Implementation Services need a recovery path for notifications that never reach the application. Intuit's Change Data Capture (CDC) functionality lets integrations retrieve changes for supported entities over a specified period.&lt;/p&gt;

&lt;p&gt;Maintain a durable checkpoint for each company and relevant entity scope. Reconcile returned changes, then advance the checkpoint only after processing succeeds.&lt;/p&gt;

&lt;p&gt;Advancing a checkpoint before persisting the changes can create permanent gaps. Keeping the checkpoint unchanged after successful processing can repeat work, so reconciliation must remain safe to repeat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing and Verification
&lt;/h2&gt;

&lt;p&gt;Quickbooks Implementation Services should include failure testing before production deployment. A successful demonstration with two notifications does not prove that the integration survives worker crashes or concurrent updates.&lt;/p&gt;

&lt;p&gt;Test these cases in a QuickBooks sandbox:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Send the same notification twice and verify one pending refresh.&lt;/li&gt;
&lt;li&gt;Send notifications with different timestamps and verify the newest timestamp remains queued.&lt;/li&gt;
&lt;li&gt;Crash a worker after queue persistence and verify the job resumes.&lt;/li&gt;
&lt;li&gt;Interrupt CDC recovery and verify the checkpoint does not skip unprocessed changes.&lt;/li&gt;
&lt;li&gt;Submit an update using a stale &lt;code&gt;SyncToken&lt;/code&gt; and verify conflict handling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Track notification identifiers, company IDs, entity types, retry counts, and reconciliation results. Never log OAuth access tokens or other credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-Offs and Limitations
&lt;/h2&gt;

&lt;p&gt;Quickbooks Implementation Services must account for the limitations of entity-level coalescing. It reduces redundant refreshes but does not preserve every intermediate state. If your application needs every business event for auditing, retain a durable event log and use an idempotent consumer.&lt;/p&gt;

&lt;p&gt;A second limitation is concurrency. An in-memory map cannot coordinate multiple application instances or survive restarts. Production systems need persistent queue storage, atomic job claims, and recovery for abandoned work.&lt;/p&gt;

&lt;p&gt;Finally, API limits and temporary failures can delay synchronization. Use bounded retries, backoff, dead-letter handling, and alerts for unresolved accounting records.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://www.oodles.com/" rel="noopener noreferrer"&gt;Oodles&lt;/a&gt;, implementation decisions should be tied to the actual integration architecture and its acceptance criteria. The code in this article remains an illustrative demonstration until validated against the production requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Treat webhooks as change notifications rather than an ordered accounting ledger.&lt;/li&gt;
&lt;li&gt;Acknowledge notifications promptly and move expensive processing into a durable queue.&lt;/li&gt;
&lt;li&gt;Coalesce duplicate notifications without suppressing legitimate updates.&lt;/li&gt;
&lt;li&gt;Reconcile current state and use CDC to recover missed changes.&lt;/li&gt;
&lt;li&gt;Persist checkpoints and test crash recovery before launch.&lt;/li&gt;
&lt;li&gt;Measure accounting accuracy and reconciliation exceptions rather than deployment completion alone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are building a QuickBooks integration, share how you handle missed notifications and reconciliation checkpoints. Our &lt;a href="https://www.oodles.com/quickbooks/7144781" rel="noopener noreferrer"&gt;QuickBooks integration services&lt;/a&gt; page provides additional context on accounting integrations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should every QuickBooks webhook create a database record?
&lt;/h3&gt;

&lt;p&gt;Not necessarily. If your application needs the latest entity state, coalescing notifications by company and entity can reduce redundant refreshes. If every intermediate event matters for auditing or business processing, retain a durable event log.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can QuickBooks webhook notifications arrive out of order?
&lt;/h3&gt;

&lt;p&gt;Yes. Intuit warns that notification ordering is not guaranteed. Reconcile against the latest authoritative entity state instead of applying notifications blindly in arrival order.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is an in-memory queue suitable for production?
&lt;/h3&gt;

&lt;p&gt;No, not when processing must survive crashes or coordinate multiple instances. Use persistent queue storage or a database-backed work table, along with retries and monitoring for abandoned work.&lt;/p&gt;

&lt;h3&gt;
  
  
  How should an integration handle a stale SyncToken?
&lt;/h3&gt;

&lt;p&gt;Fetch the current entity state and resolve the conflict before retrying the update. Do not repeatedly submit the same stale token. Verify the conflict response against the current API documentation for the specific entity.&lt;/p&gt;

&lt;h3&gt;
  
  
  When should an integration use CDC?
&lt;/h3&gt;

&lt;p&gt;CDC provides a recovery path for changes that webhooks missed or the application failed to process. Use durable checkpoints, reconcile the returned changes, and verify that recovery does not skip unprocessed records.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to Build Reliable Zoho Integration Services with Node.js</title>
      <dc:creator>Sanya Mittal</dc:creator>
      <pubDate>Tue, 06 Oct 2026 05:20:01 +0000</pubDate>
      <link>https://dev.to/sanya_mittal_a509a2c50a2d/how-to-build-reliable-zoho-integration-services-with-nodejs-1ek</link>
      <guid>https://dev.to/sanya_mittal_a509a2c50a2d/how-to-build-reliable-zoho-integration-services-with-nodejs-1ek</guid>
      <description>&lt;p&gt;An implementation guide for webhook ingestion, idempotency, API limits, and failure recovery.&lt;/p&gt;

&lt;p&gt;A Zoho webhook can trigger the right workflow and still create duplicate records or incomplete transactions. This happens when a Node.js integration treats every webhook as a new event instead of handling retries and repeated deliveries.&lt;/p&gt;

&lt;p&gt;This guide explains a practical pattern for building Zoho Integration services around webhook ingestion, idempotency, API limits, and controlled retries. We will use Node.js, Express, PostgreSQL, and Redis in the reference architecture.&lt;/p&gt;

&lt;p&gt;For broader implementation patterns, see &lt;a href="https://www.oodles.com/zoho/7144783?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=backlink&amp;amp;utm_content=devto_article_29" rel="noopener noreferrer"&gt;how Zoho Integration services connect enterprise applications&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context and Setup
&lt;/h2&gt;

&lt;p&gt;The system receives events from Zoho CRM and updates an external application. A typical flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Zoho CRM
   |
   | Webhook
   v
Node.js API
   |
   +--&amp;gt; Redis: idempotency check
   |
   +--&amp;gt; PostgreSQL: event state
   |
   v
External API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zoho Integration services supports webhooks for sending event notifications to third-party applications. Its documentation also states that a failed webhook can trigger another notification after 15 minutes. This makes duplicate-safe processing important.&lt;/p&gt;

&lt;p&gt;Zoho's APIs also impose request limits. For example, Zoho Books documents a limit of 100 requests per minute per organization. It returns HTTP 429 when the rate limit is exceeded.&lt;/p&gt;

&lt;p&gt;That means the integration needs more than an HTTP endpoint. It needs state management.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building Zoho Integration Services with Idempotent Webhooks
&lt;/h2&gt;

&lt;p&gt;The solution is to separate webhook acceptance from business processing. The endpoint should validate the request, record the event, and prevent the same event from executing twice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Validate and acknowledge the webhook
&lt;/h3&gt;

&lt;p&gt;Do not perform several downstream API calls before returning from the webhook endpoint.&lt;/p&gt;

&lt;p&gt;First, validate the request and identify the event.&lt;/p&gt;

&lt;p&gt;For custom webhook authentication, we can configure a shared secret in Zoho Integration services webhook headers. Zoho supports custom header parameters for webhook requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/zoho&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-webhook-token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Why: reject requests that do not contain our configured secret.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ZOHO_WEBHOOK_TOKEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eventId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-event-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Why: the event ID becomes the idempotency key.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Missing event ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Store the event before starting downstream processing.&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;saveEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact authentication mechanism should match what the Zoho product and webhook configuration expose. Do not assume that every Zoho webhook provides a cryptographic signature.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Make event processing idempotent
&lt;/h3&gt;

&lt;p&gt;The second step is preventing repeated events from creating repeated business records.&lt;/p&gt;

&lt;p&gt;PostgreSQL can enforce this at the database level.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;integration_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use the event ID as a unique key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`INSERT INTO integration_events (event_id, payload)
   VALUES ($1, $2)
   ON CONFLICT (event_id) DO NOTHING
   RETURNING event_id`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Why: an empty result means this event was already accepted.&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rowCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;duplicate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is safer than keeping duplicate checks only in application memory. A process restart should not erase the integration's event history.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Control downstream API calls
&lt;/h3&gt;

&lt;p&gt;The third step is protecting Zoho and external APIs from retry storms.&lt;/p&gt;

&lt;p&gt;A worker can consume pending events and process them with controlled concurrency.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updateExternalSystem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Why: mark completion only after the downstream request succeeds.&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;markCompleted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Why: retain the event so a worker can retry it safely.&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;markForRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not immediately retry every failure.&lt;/p&gt;

&lt;p&gt;Classify errors first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;400-level validation errors:&lt;/strong&gt; usually require correction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;401 or 403 errors:&lt;/strong&gt; require authentication or permission handling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;429 errors:&lt;/strong&gt; require throttling and delayed retry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5xx errors:&lt;/strong&gt; can usually enter controlled retry processing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network timeouts:&lt;/strong&gt; should use bounded retries.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For high-volume integrations, Redis can manage short-lived locks and queue state. PostgreSQL should remain the durable record of processing state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Application
&lt;/h2&gt;

&lt;p&gt;In one of our Zoho Integration services projects at Oodles, we connected Zoho Inventory and Zoho Books with the Yango API, Shopify, and analytics.&lt;/p&gt;

&lt;p&gt;The problem was not a single API call. The workflow required data to move between four systems while keeping the Zoho applications focused on their respective business functions.&lt;/p&gt;

&lt;p&gt;We implemented custom middleware as the Zoho Integration services boundary. The middleware handled external-system translation and orchestration while Zoho Inventory and Books remained part of the operational workflow.&lt;/p&gt;

&lt;p&gt;The measurable architectural result was a single middleware boundary coordinating four participating systems. This avoided placing external integration logic separately inside each Zoho workflow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.oodles.com" rel="noopener noreferrer"&gt;Oodles&lt;/a&gt; applies this pattern when an integration needs centralized orchestration rather than isolated application-to-application workflows.&lt;/p&gt;

&lt;p&gt;The same design can be extended with event queues, dead-letter processing, distributed tracing, and reconciliation jobs when transaction volume increases.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Acknowledge webhooks quickly.&lt;/strong&gt; Move longer processing into asynchronous workers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use idempotency keys.&lt;/strong&gt; A repeated event should produce the same business result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persist event state.&lt;/strong&gt; Application memory is not a durable integration ledger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respect API limits.&lt;/strong&gt; Zoho Integration services documents rate limits and HTTP 429 responses for exceeded limits. ([Zoho][2])&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate validation from retry logic.&lt;/strong&gt; Invalid data should not enter an endless retry loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Have a webhook, API, or synchronization problem in your Zoho environment? Share your architecture or failure scenario in the comments, or discuss your &lt;a href="https://www.oodles.com/contact-us" rel="noopener noreferrer"&gt;Zoho Integration services&lt;/a&gt; requirements with our integration engineers.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I prevent duplicate Zoho webhook processing?
&lt;/h3&gt;

&lt;p&gt;Use a persistent event identifier as an idempotency key. Store that identifier in PostgreSQL with a unique constraint before starting downstream processing. If the same event arrives again, the database rejects the duplicate and the worker skips business execution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Zoho retry failed webhooks?
&lt;/h3&gt;

&lt;p&gt;Yes. Zoho CRM documentation states that after a webhook failure, another notification can be sent after 15 minutes. Integrations should therefore tolerate repeated delivery instead of assuming each webhook represents a unique execution.&lt;/p&gt;

&lt;h3&gt;
  
  
  What causes HTTP 429 errors in Zoho integrations?
&lt;/h3&gt;

&lt;p&gt;HTTP 429 indicates that an API rate or concurrency limit has been exceeded. The integration should reduce request concurrency, delay retries, and process queued work according to the relevant Zoho API limits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should Zoho webhooks use a queue?
&lt;/h3&gt;

&lt;p&gt;A queue is useful when webhook processing requires multiple downstream API calls or can exceed the expected request duration. The webhook endpoint can persist the event and return quickly while a worker handles business processing asynchronously.&lt;/p&gt;

&lt;h3&gt;
  
  
  When should I use middleware for Zoho Integration services?
&lt;/h3&gt;

&lt;p&gt;Middleware becomes useful when several applications participate in the same workflow. It can centralize transformation, authentication, retries, logging, event state, and reconciliation instead of distributing those responsibilities across individual Zoho workflows.&lt;/p&gt;

</description>
      <category>zoho</category>
      <category>integration</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>ERP Integration Services: Fixing Event Order</title>
      <dc:creator>Sanya Mittal</dc:creator>
      <pubDate>Fri, 02 Oct 2026 05:52:33 +0000</pubDate>
      <link>https://dev.to/sanya_mittal_a509a2c50a2d/erp-integration-services-fixing-event-order-4kpe</link>
      <guid>https://dev.to/sanya_mittal_a509a2c50a2d/erp-integration-services-fixing-event-order-4kpe</guid>
      <description>&lt;p&gt;We once saw an ERP Integration Services return successful API responses while the business workflow was still wrong.&lt;/p&gt;

&lt;p&gt;The order reached the ERP. The warehouse received the inventory request. The CRM received the customer update. Every HTTP call returned &lt;code&gt;2xx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The failure appeared later.&lt;/p&gt;

&lt;p&gt;The warehouse processed an order before its customer record was fully synchronized. Reconciliation then required manual checks because each system had accepted a different part of the transaction.&lt;/p&gt;

&lt;p&gt;This is the part of &lt;a href="https://www.oodles.com/erp-integration-services/4344175?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=backlink&amp;amp;utm_content=devto_article_25" rel="noopener noreferrer"&gt;ERP Integration Services that API documentation rarely solves&lt;/a&gt;. The difficult problem is not moving JSON between systems. It is preserving business-event order when systems fail independently.&lt;/p&gt;

&lt;p&gt;In this article, we will build that reasoning into the ERP Integration Services design. The examples use Python 3.11+ and asynchronous HTTP patterns. The code is intentionally simplified so the sequencing problem stays visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Start With the Business Event, Not the API
&lt;/h2&gt;

&lt;p&gt;The first mistake is usually visible in the controller.&lt;/p&gt;

&lt;p&gt;A developer receives an order and immediately calls three systems:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Naive ERP Integration Services flow: every downstream call happens inline.
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;erp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reserve_stock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;crm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks reasonable until the warehouse call fails.&lt;/p&gt;

&lt;p&gt;The ERP order now exists. The warehouse reservation does not. A retry of the whole function can create another ERP order unless the operation is idempotent.&lt;/p&gt;

&lt;p&gt;The bigger issue is ordering.&lt;/p&gt;

&lt;p&gt;The actual dependency may be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer confirmed
       |
       v
Order accepted
       |
       v
Inventory reserved
       |
       v
Shipment confirmed
       |
       v
Invoice posted
       |
       v
Payment reconciled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sequence should become an explicit state model.&lt;/p&gt;

&lt;p&gt;Microsoft's Business Central documentation describes business events around changes in process state. It specifically gives a posted sales order as an example of a business event.&lt;/p&gt;

&lt;p&gt;This is a useful distinction.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;POST /orders&lt;/code&gt; is an API operation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;OrderPosted&lt;/code&gt; is a business event.&lt;/p&gt;

&lt;p&gt;The second carries more meaning to downstream consumers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Separate the Transaction From Its Side Effects
&lt;/h2&gt;

&lt;p&gt;Once the event sequence is explicit, the next problem is atomicity.&lt;/p&gt;

&lt;p&gt;Your ERP database and your CRM database do not share a transaction. Neither should pretend they do.&lt;/p&gt;

&lt;p&gt;A common workaround is to make the HTTP request part of the original transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The problem: the database transaction depends on an external network call.
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;crm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mark_synced&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the CRM takes 10 seconds, your application waits 10 seconds.&lt;/p&gt;

&lt;p&gt;If the CRM returns &lt;code&gt;503&lt;/code&gt;, your local transaction now depends on whether the caller retries correctly.&lt;/p&gt;

&lt;p&gt;We instead persist the business event locally and process the external call separately.&lt;/p&gt;

&lt;p&gt;A minimal outbox table can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- The important part is storing the event before attempting external delivery.&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;integration_outbox&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;aggregate_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application then commits the order and event together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The order and its integration event commit as one local transaction.
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO orders(id, status) VALUES (?, ?)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confirmed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        INSERT INTO integration_outbox
        (event_type, aggregate_id, payload)
        VALUES (?, ?, ?)
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OrderConfirmed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python's SQLite documentation confirms that explicit &lt;code&gt;commit()&lt;/code&gt; and &lt;code&gt;rollback()&lt;/code&gt; control pending transactions when using the standard transaction mode.&lt;/p&gt;

&lt;p&gt;The important change is architectural.&lt;/p&gt;

&lt;p&gt;The request no longer needs the CRM to succeed before the order can become confirmed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Make the Worker Idempotent
&lt;/h2&gt;

&lt;p&gt;Persisting an event fixes one failure mode. It does not prevent duplicate delivery.&lt;/p&gt;

&lt;p&gt;A worker may successfully call the ERP and crash before marking the event as processed.&lt;/p&gt;

&lt;p&gt;The next worker execution can therefore receive the same event.&lt;/p&gt;

&lt;p&gt;That is expected in many asynchronous systems.&lt;/p&gt;

&lt;p&gt;The consumer must handle it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ERP Integration Services should treat duplicate delivery as a normal condition.
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;already_processed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;post_to_erp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;mark_processed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a race in this simplified version.&lt;/p&gt;

&lt;p&gt;Two workers can read &lt;code&gt;already_processed(event.id)&lt;/code&gt; before either writes the processed record.&lt;/p&gt;

&lt;p&gt;The production version should enforce uniqueness in the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- The database becomes the final guard against duplicate event processing.&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;ux_processed_events&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;processed_events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the worker can attempt to record the event inside a transaction.&lt;/p&gt;

&lt;p&gt;This matters more than adding another retry library. Retries without idempotency can turn a temporary outage into duplicate invoices, duplicate customers, or duplicate inventory movements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Use Webhooks for Notification, Not Workflow Ownership
&lt;/h2&gt;

&lt;p&gt;Once the worker handles events independently, teams often move everything to webhooks.&lt;/p&gt;

&lt;p&gt;That introduces another trap.&lt;/p&gt;

&lt;p&gt;A webhook tells you that something changed. It does not necessarily tell you that every dependent workflow has completed.&lt;/p&gt;

&lt;p&gt;Odoo 19, for example, supports webhook-triggered automation and its external JSON-2 API exposes model methods through &lt;code&gt;/json/2/&amp;lt;model&amp;gt;/&amp;lt;method&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Odoo 19 External JSON-2 API documentation&lt;/p&gt;

&lt;p&gt;For example, a webhook can produce:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order.confirmed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"order_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SO-10482"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The consumer should not immediately assume that inventory, payment, and fulfillment are ready.&lt;/p&gt;

&lt;p&gt;Instead, it should query or consume the next authoritative state.&lt;/p&gt;

&lt;p&gt;Business Central follows a similar model. Its webhook subscriptions notify consumers about entity changes, and Microsoft documents retries when the subscriber returns &lt;code&gt;408&lt;/code&gt;, &lt;code&gt;429&lt;/code&gt;, or &lt;code&gt;5xx&lt;/code&gt; responses.&lt;/p&gt;

&lt;p&gt;That retry behavior is useful, but it does not remove the need for idempotency.&lt;/p&gt;

&lt;p&gt;A retry means the same notification can reach your service again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Keep Slow ERP Calls Outside the Request Path
&lt;/h2&gt;

&lt;p&gt;The previous steps separate event persistence from delivery. The final piece is execution.&lt;/p&gt;

&lt;p&gt;Do not make an API consumer wait while several ERP operations complete unless the caller genuinely needs the result synchronously.&lt;/p&gt;

&lt;p&gt;With Python 3.11+, &lt;code&gt;asyncio.TaskGroup&lt;/code&gt; provides structured concurrency for related asynchronous tasks.&lt;/p&gt;

&lt;p&gt;For independent work, that can look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;notify_downstream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TaskGroup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;tg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;update_crm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;tg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;update_analytics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But do not use concurrency when the operations have dependencies.&lt;/p&gt;

&lt;p&gt;This is wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Do not parallelize events that depend on a specific business sequence.
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TaskGroup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;create_customer&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;tg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;create_order&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;tg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;reserve_inventory&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;create_order()&lt;/code&gt; requires the customer ID, concurrency has changed the business semantics.&lt;/p&gt;

&lt;p&gt;The correct boundary is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order confirmed
      |
      +--&amp;gt; CRM notification
      |
      +--&amp;gt; Analytics event

Inventory reserved
      |
      +--&amp;gt; Warehouse notification
      |
      +--&amp;gt; Shipment workflow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Concurrency belongs inside an independent stage. It should not replace the dependency graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Application: Where This Changed the Outcome
&lt;/h2&gt;

&lt;p&gt;That distinction between concurrency and dependency mattered in an &lt;a href="//oodles.com"&gt;Oodles&lt;/a&gt; logistics ERP implementation. The integration had to coordinate order processing, inventory, shipment workflows, and operational reporting.&lt;/p&gt;

&lt;p&gt;We initially treated synchronization as data movement between systems. The harder problem appeared when different systems accepted updates at different points in the transaction.&lt;/p&gt;

&lt;p&gt;We changed the workflow to use shared transaction states rather than treating each API response as completion. Downstream actions followed those states instead of blindly following the original request.&lt;/p&gt;

&lt;p&gt;The result was a 45% reduction in order-processing time, 65% lower manual effort, and 99.5% inventory accuracy. Operational errors also fell by 35%.&lt;/p&gt;

&lt;p&gt;The important lesson was not simply to add asynchronous processing. We had to preserve the business sequence first. Only then could individual stages run independently.&lt;/p&gt;

&lt;p&gt;That is the trade-off behind good ERP Integration Services. More state and event handling adds implementation work. In return, failures become recoverable instead of becoming reconciliation exercises.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens in Production
&lt;/h2&gt;

&lt;p&gt;The happy path is rarely the difficult part.&lt;/p&gt;

&lt;p&gt;Production introduces expired credentials, ERP maintenance, rate limits, duplicate webhooks, partial database failures, and downstream timeouts.&lt;/p&gt;

&lt;p&gt;We therefore monitor the event lifecycle rather than only HTTP status codes.&lt;/p&gt;

&lt;p&gt;A useful event record should answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;event_id
event_type
aggregate_id
created_at
attempt_count
last_attempt_at
status
error_code
error_message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When an operator sees &lt;code&gt;OrderConfirmed&lt;/code&gt; stuck in &lt;code&gt;retrying&lt;/code&gt;, they should not need to inspect five application logs to understand what happened.&lt;/p&gt;

&lt;p&gt;The &lt;a href="//oodles.com/contact-us"&gt;ERP integration services&lt;/a&gt; system should make the failure visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Model ERP workflows as business states before designing API calls.&lt;/li&gt;
&lt;li&gt;Commit the business transaction and its integration event together.&lt;/li&gt;
&lt;li&gt;Assume webhook and queue delivery can happen more than once.&lt;/li&gt;
&lt;li&gt;Enforce idempotency with a database constraint, not only application logic.&lt;/li&gt;
&lt;li&gt;Use concurrency only after identifying which operations are truly independent.&lt;/li&gt;
&lt;li&gt;Monitor event state, retry count, and reconciliation status instead of HTTP success alone.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What are ERP Integration Services from a developer's perspective?
&lt;/h3&gt;

&lt;p&gt;ERP Integration Services connect ERP APIs, databases, webhooks, queues, and external applications into a controlled workflow. Developers must handle authentication, data mapping, event ordering, retries, duplicate delivery, monitoring, and reconciliation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should ERP integrations be synchronous or asynchronous?
&lt;/h3&gt;

&lt;p&gt;Use synchronous calls when the caller needs an immediate result. Use asynchronous processing for workflows that can tolerate delayed completion or involve unreliable downstream systems. Many ERP integrations need both patterns at different stages.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you prevent duplicate ERP records?
&lt;/h3&gt;

&lt;p&gt;Use an idempotency key or unique event identifier. Store processed identifiers with a database uniqueness constraint. The consumer can then safely receive the same event again without creating another order, invoice, or customer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are webhooks enough for ERP integration?
&lt;/h3&gt;

&lt;p&gt;No. Webhooks provide notifications, but they do not define the complete business workflow. Your integration still needs state handling, validation, retries, idempotency, and reconciliation when downstream processing fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  When should developers use middleware for ERP integration?
&lt;/h3&gt;

&lt;p&gt;In ERP Integration Services Middleware becomes useful when several systems share routing, transformation, authentication, retry, monitoring, or orchestration logic. A direct API connection can remain simpler when only two stable systems exchange limited data.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>QuickBooks Integration Services: Stop Duplicate Invoices</title>
      <dc:creator>Sanya Mittal</dc:creator>
      <pubDate>Tue, 29 Sep 2026 06:57:22 +0000</pubDate>
      <link>https://dev.to/sanya_mittal_a509a2c50a2d/quickbooks-integration-services-stop-duplicate-invoices-4d1l</link>
      <guid>https://dev.to/sanya_mittal_a509a2c50a2d/quickbooks-integration-services-stop-duplicate-invoices-4d1l</guid>
      <description>&lt;p&gt;The dangerous QuickBooks Online failure is not always a &lt;code&gt;400&lt;/code&gt; or &lt;code&gt;429&lt;/code&gt;. It is the request that creates an invoice, then times out before your application receives the response.&lt;/p&gt;

&lt;p&gt;A worker sees the timeout and retries. QuickBooks may already contain the invoice. Your worker does not know that, so it creates another one.&lt;/p&gt;

&lt;p&gt;This is where QuickBooks Integrations become an application-design problem. The fix is not another retry loop. QuickBooks Integration Services need a durable source transaction ID, a unique local mapping, controlled retries, and reconciliation after uncertain outcomes.&lt;/p&gt;

&lt;p&gt;We will build that flow with Python 3.11+, PostgreSQL, and the QuickBooks Online REST API. The examples focus on invoice synchronization because the same failure appears when connecting QBO with ERP, CRM, billing, or order-management systems.&lt;/p&gt;

&lt;p&gt;For the broader implementation context, see the &lt;a href="https://www.oodles.com/quickbooks/7144781?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=backlink&amp;amp;utm_content=devto_article_21" rel="noopener noreferrer"&gt;QuickBooks Integration Services service page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Do Not Retry QuickBooks Integration Services Blindly
&lt;/h2&gt;

&lt;p&gt;The duplicate starts with an innocent-looking retry. A naive worker treats every failed HTTP call as proof that QuickBooks rejected the operation. That assumption fails when the connection breaks after the server has processed the request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Python 3.11+: this naive QuickBooks Integration Services retry can create two invoices.
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qbo_invoice_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;invoice_payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qbo_invoice_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;invoice_payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second POST has no knowledge of the first POST's outcome. It is simply another create operation.&lt;/p&gt;

&lt;p&gt;QuickBooks documents &lt;code&gt;SyncToken&lt;/code&gt; for updating existing objects, but that token does not make invoice creation idempotent. The application needs its own transaction identity.&lt;/p&gt;

&lt;p&gt;A common production symptom is &lt;code&gt;6140 Duplicate Document Number Error&lt;/code&gt;. Developers often try to solve that error with another pre-check. That misses the larger problem. The integration needs to know whether it already owns the QBO invoice.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Give QuickBooks Integration Services a Durable Identity
&lt;/h2&gt;

&lt;p&gt;Once the retry can no longer distinguish success from failure, the next decision is where that identity lives.&lt;/p&gt;

&lt;p&gt;We normally create an integration mapping table with a unique constraint on the source transaction. The source system might provide &lt;code&gt;order_id&lt;/code&gt;, &lt;code&gt;invoice_id&lt;/code&gt;, or another immutable identifier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- PostgreSQL 14+: the unique constraint prevents two workers claiming one source invoice.&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;qbo_invoice_map&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;source_invoice_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;qbo_invoice_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important field is not &lt;code&gt;qbo_invoice_id&lt;/code&gt;. It is &lt;code&gt;source_invoice_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Before creating an invoice, the worker claims that source transaction. A second worker sees the existing record instead of independently creating another invoice.&lt;/p&gt;

&lt;p&gt;This pattern also addresses a recurring integration problem. Application state should record what the system attempted to send, rather than relying only on a QuickBooks-side duplicate search.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Separate “Request Failed” From “Outcome Unknown”
&lt;/h2&gt;

&lt;p&gt;Once the mapping exists, the remaining problem is a process crash between the API write and the database update.&lt;/p&gt;

&lt;p&gt;Imagine this sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Worker inserts &lt;code&gt;INV-1007&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Worker sends the invoice to QBO.&lt;/li&gt;
&lt;li&gt;QBO creates invoice &lt;code&gt;381&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The worker loses its network connection.&lt;/li&gt;
&lt;li&gt;The worker never stores &lt;code&gt;qbo_invoice_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A retry starts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The database says &lt;code&gt;INV-1007&lt;/code&gt; exists, but it does not know the QBO ID.&lt;/p&gt;

&lt;p&gt;That state needs its own meaning.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Python 3.11+: UNKNOWN means the worker must reconcile before issuing another create.
&lt;/span&gt;&lt;span class="n"&gt;ALLOWED_STATES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PENDING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SYNCED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UNKNOWN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAILED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;should_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qbo_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PENDING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;qbo_id&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;UNKNOWN&lt;/code&gt;, we reconcile first. We do not automatically create another invoice.&lt;/p&gt;

&lt;p&gt;If the application uses a controlled &lt;code&gt;DocNumber&lt;/code&gt;, it can query using the source-derived document number and inspect the result. Intuit documents &lt;code&gt;DocNumber&lt;/code&gt; behavior and duplicate document-number handling.&lt;/p&gt;

&lt;p&gt;This is an important trade-off. A local mapping gives stronger application control. A QBO query can recover after a crash. Using both gives a better recovery path than either mechanism alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Treat 429 as a Queueing Problem
&lt;/h2&gt;

&lt;p&gt;Once duplicate creation is controlled, QuickBooks Integration face another constraint: request volume.&lt;/p&gt;

&lt;p&gt;Intuit currently documents a production limit of 500 REST requests per minute per realm and 10 requests per second per realm and app. A throttled request returns HTTP &lt;code&gt;429&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That changes how a worker should behave.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Python 3.11+: the queue absorbs throttling instead of making every request retry immediately.
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;schedule_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delay_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;available_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;delay_seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not put a &lt;code&gt;while True&lt;/code&gt; loop around the HTTP request. That keeps the worker occupied while the request remains unavailable.&lt;/p&gt;

&lt;p&gt;We prefer a durable queue with delayed jobs. Workers can process other realms while one realm waits for its QBO quota.&lt;/p&gt;

&lt;p&gt;Intuit also documents a maximum of 30 payloads in one batch request. Batch processing can reduce request overhead, but each payload still needs individual result handling.&lt;/p&gt;

&lt;p&gt;See the official QuickBooks API limits and throttles documentation for the current platform limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Use Webhooks for Change Detection
&lt;/h2&gt;

&lt;p&gt;The rate limit makes constant polling expensive. QuickBooks Integration Services can use webhooks as another signal when QBO data changes.&lt;/p&gt;

&lt;p&gt;Intuit's webhook documentation lists invoices among supported entities. Notifications include an event ID, entity ID, and QuickBooks company ID. Signature validation uses HMAC-SHA256 and the app's verifier token.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Python 3.11+: verify the raw request body before accepting the webhook event.
&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;verifier_token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;raw_body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;intuit-signature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We treat the event as a trigger to reconcile state, not as proof that our database is correct.&lt;/p&gt;

&lt;p&gt;A webhook can tell us that invoice &lt;code&gt;381&lt;/code&gt; changed. The integration database still needs to decide whether that change belongs to the source transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Application
&lt;/h2&gt;

&lt;p&gt;That distinction between duplicate prevention and recovery became central in an &lt;a href="//oodles.com"&gt;Oodles&lt;/a&gt; QuickBooks Integration Services project for a healthcare billing workflow.&lt;/p&gt;

&lt;p&gt;We worked with CSV billing records that had to become QBO invoices. The workflow needed grouping, field mapping, duplicate detection, conditional invoice updates, and protection for invoices that already had payments.&lt;/p&gt;

&lt;p&gt;We used OAuth 2.0 for QBO connectivity and rules around invoice state before allowing updates. The public project record documents these controls, but it does not publish a before-and-after latency or error-rate figure.&lt;/p&gt;

&lt;p&gt;The trade-off was explicit. Automatic synchronization was useful only when the integration could avoid changing a financially settled invoice.&lt;/p&gt;

&lt;p&gt;We therefore treated duplicate detection and payment-aware updates as separate rules. A new billing record could create an invoice. An existing invoice could be updated only when its state allowed that operation.&lt;/p&gt;

&lt;p&gt;The project record does not publish a quantified business outcome. We will not invent one.&lt;/p&gt;

&lt;p&gt;That missing metric is itself useful. Integration projects should measure recovery outcomes, not only whether API calls return &lt;code&gt;200&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;QuickBooks Integration Services need a durable source transaction ID before creating financial records.&lt;/li&gt;
&lt;li&gt;Production QuickBooks Integration Services should model unknown outcomes instead of treating every timeout as a failed write.&lt;/li&gt;
&lt;li&gt;Store an &lt;code&gt;UNKNOWN&lt;/code&gt; state when the application cannot determine whether QBO accepted a request.&lt;/li&gt;
&lt;li&gt;Treat HTTP &lt;code&gt;429&lt;/code&gt; as a queueing and scheduling problem rather than a tight retry loop.&lt;/li&gt;
&lt;li&gt;Use QBO webhooks to trigger reconciliation while keeping operational state in your integration database.&lt;/li&gt;
&lt;li&gt;It should measure recovery time and duplicate prevention, not only successful API responses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your CRM, ERP, ecommerce, or billing system needs controlled financial synchronization, connect with us to discuss your &lt;a href="//oodles.com/contact-us"&gt;QuickBooks Integration Services&lt;/a&gt; requirements.&lt;/p&gt;

&lt;h1&gt;
  
  
  What Are QuickBooks Integration Services?
&lt;/h1&gt;

&lt;p&gt;It is connect QuickBooks Online with CRM, ERP, ecommerce, billing, payroll, or custom applications. Production work covers authentication, mapping, synchronization, error handling, duplicate prevention, and reconciliation rather than only sending API requests.&lt;/p&gt;

&lt;h1&gt;
  
  
  How Do QuickBooks Integration Prevent Duplicate Invoices?
&lt;/h1&gt;

&lt;p&gt;They store the QuickBooks ID and source transaction ID in an integration database. Before creating an invoice, the service checks that mapping. Retry logic then reuses the existing transaction instead of creating another invoice after a timeout.&lt;/p&gt;

&lt;h1&gt;
  
  
  Can QuickBooks Online Sync With A Custom ERP?
&lt;/h1&gt;

&lt;p&gt;Yes. A custom ERP can exchange supported customers, items, invoices, payments, and other entities through the QuickBooks Online API. The architecture should define ownership, mappings, synchronization direction, retries, and reconciliation before production deployment.&lt;/p&gt;

&lt;h1&gt;
  
  
  Should QuickBooks Integration Be Real Time?
&lt;/h1&gt;

&lt;p&gt;Not every record needs real-time synchronization. Real-time events suit changes that affect active workflows. Batch synchronization can suit historical imports and lower-priority reporting data. The choice depends on urgency, API limits, and recovery requirements.&lt;/p&gt;

&lt;h1&gt;
  
  
  How Much Do QuickBooks Services Cost?
&lt;/h1&gt;

&lt;p&gt;Cost depends on systems, entities, data volume, synchronization direction, business rules, and testing. A two-system invoice flow can be much smaller than an integration covering CRM, ERP, ecommerce, payments, and historical migration.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>qbo</category>
      <category>programming</category>
    </item>
    <item>
      <title>ERP Consulting Services: Fix ERP API Sync Failures</title>
      <dc:creator>Sanya Mittal</dc:creator>
      <pubDate>Thu, 24 Sep 2026 06:15:18 +0000</pubDate>
      <link>https://dev.to/sanya_mittal_a509a2c50a2d/erp-consulting-services-fix-erp-api-sync-failures-406</link>
      <guid>https://dev.to/sanya_mittal_a509a2c50a2d/erp-consulting-services-fix-erp-api-sync-failures-406</guid>
      <description>&lt;p&gt;A sync job can look healthy while quietly creating duplicate ERP records.&lt;/p&gt;

&lt;p&gt;We have seen this class of problem when an external system retries an ERP consulting services request after a timeout. The client sees a failed request, retries it, and the ERP receives both requests. The result can be duplicate customers, orders, invoices, or payments.&lt;/p&gt;

&lt;p&gt;This is where ERP consulting services need to go beyond module selection. The integration contract, retry behavior, API version, and record identity should be decided before implementation starts.&lt;/p&gt;

&lt;p&gt;This article walks through one practical pattern: &lt;a href="https://www.oodles.com/custom-erp/11/solutions-explainer?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=backlink&amp;amp;utm_content=devto_article_20" rel="noopener noreferrer"&gt;designing ERP integrations around idempotency, bounded retries&lt;/a&gt;, and explicit API contracts. The examples use Odoo because its current API direction makes this decision especially relevant.&lt;/p&gt;

&lt;p&gt;Odoo 19 introduced its External JSON-2 API. Odoo also deprecated the older XML-RPC and JSON-RPC external APIs in version 19.&lt;/p&gt;

&lt;p&gt;That API change is not just a developer concern. It changes what an ERP implementation team should review during technical discovery.&lt;/p&gt;

&lt;h1&gt;
  
  
  Step 1: Find the failure before adding retries
&lt;/h1&gt;

&lt;p&gt;The first mistake is usually simple:&lt;/p&gt;

&lt;p&gt;The integration receives a timeout and assumes the ERP rejected the transaction.&lt;/p&gt;

&lt;p&gt;A timeout only tells us that the caller did not receive the response. It does not prove that the server did not process the request.&lt;/p&gt;

&lt;p&gt;Consider this simplified Python example.&lt;/p&gt;

&lt;p&gt;import requests&lt;/p&gt;

&lt;h1&gt;
  
  
  ERP consulting services should identify whether a retry can duplicate a write.
&lt;/h1&gt;

&lt;p&gt;response = requests.post(&lt;br&gt;
    "&lt;a href="https://erp.example.com/json/2/res.partner/create" rel="noopener noreferrer"&gt;https://erp.example.com/json/2/res.partner/create&lt;/a&gt;",&lt;br&gt;
    headers={"Authorization": "bearer YOUR_API_KEY"},&lt;br&gt;
    json={"name": "Acme Ltd"},&lt;br&gt;
    timeout=10,&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;response.raise_for_status()&lt;/p&gt;

&lt;p&gt;The problem is not the HTTP request itself.&lt;/p&gt;

&lt;p&gt;The problem is what happens when the client receives a 504, connection reset, or local timeout after the ERP has already created the record.&lt;/p&gt;

&lt;p&gt;Retrying the same write can create a second record.&lt;/p&gt;

&lt;p&gt;Before adding retry logic, we therefore need a stable business identifier. For example, an external customer ID can become the identity boundary between the CRM and ERP.&lt;/p&gt;

&lt;p&gt;That gives us the first design rule:&lt;/p&gt;

&lt;p&gt;Never make retry behavior independent of record identity.&lt;/p&gt;

&lt;h1&gt;
  
  
  Step 2: Make the operation idempotent
&lt;/h1&gt;

&lt;p&gt;Once the failure mode is clear, the next decision is where duplicate protection belongs.&lt;/p&gt;

&lt;p&gt;For integrations, we generally want the middleware to know whether an external event has already been processed. A simple database table can hold the source system, event ID, target record ID, and processing state.&lt;/p&gt;

&lt;p&gt;The write then becomes two operations:&lt;/p&gt;

&lt;p&gt;Check whether the event already exists.&lt;br&gt;
Process it only when it has not been committed.&lt;/p&gt;

&lt;p&gt;A simplified implementation looks like this:&lt;/p&gt;

&lt;p&gt;from dataclasses import dataclass&lt;/p&gt;

&lt;p&gt;@dataclass&lt;br&gt;
class SyncEvent:&lt;br&gt;
    event_id: str&lt;br&gt;
    external_id: str&lt;br&gt;
    status: str = "pending"&lt;/p&gt;

&lt;h1&gt;
  
  
  ERP consulting services should define this identity before building the connector.
&lt;/h1&gt;

&lt;p&gt;events: dict[str, SyncEvent] = {}&lt;/p&gt;

&lt;p&gt;def should_process(event_id: str) -&amp;gt; bool:&lt;br&gt;
    return event_id not in events&lt;/p&gt;

&lt;p&gt;def mark_processed(event_id: str, external_id: str) -&amp;gt; None:&lt;br&gt;
    events[event_id] = SyncEvent(&lt;br&gt;
        event_id=event_id,&lt;br&gt;
        external_id=external_id,&lt;br&gt;
        status="processed",&lt;br&gt;
    )&lt;/p&gt;

&lt;p&gt;This example uses memory for clarity. Production middleware should store the event state in a durable database with a unique constraint on event_id.&lt;/p&gt;

&lt;p&gt;That constraint matters more than the Python dictionary.&lt;/p&gt;

&lt;p&gt;Two workers can read “not found” at almost the same time. A database uniqueness constraint gives us the final guard against concurrent processing.&lt;/p&gt;

&lt;p&gt;For a financial transaction, we would also store the ERP record identifier after successful creation. That makes reconciliation possible without searching by description or amount.&lt;/p&gt;

&lt;h1&gt;
  
  
  Step 3: Treat retries as part of the API contract
&lt;/h1&gt;

&lt;p&gt;Idempotency prevents duplicate processing, but it does not solve transient failures.&lt;/p&gt;

&lt;p&gt;The next question is how aggressively the connector should retry.&lt;/p&gt;

&lt;p&gt;Microsoft's Business Central documentation explicitly calls out 429, 503, and 504 as conditions that integrations need to handle. It recommends retry strategies such as exponential backoff and queue-based traffic smoothing.&lt;/p&gt;

&lt;p&gt;That gives us a useful production pattern:&lt;/p&gt;

&lt;p&gt;import time&lt;br&gt;
import requests&lt;/p&gt;

&lt;p&gt;RETRYABLE = {429, 503, 504}&lt;/p&gt;

&lt;p&gt;def post_with_retry(url, payload, headers, attempts=4):&lt;br&gt;
    # Retry only transient failures, while keeping the operation idempotent.&lt;br&gt;
    for attempt in range(attempts):&lt;br&gt;
        response = requests.post(&lt;br&gt;
            url,&lt;br&gt;
            json=payload,&lt;br&gt;
            headers=headers,&lt;br&gt;
            timeout=30,&lt;br&gt;
        )&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    if response.status_code not in RETRYABLE:
        response.raise_for_status()
        return response

    if attempt == attempts - 1:
        response.raise_for_status()

    time.sleep(2 ** attempt)

raise RuntimeError("Request failed after retries")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The important part is not 2 ** attempt.&lt;/p&gt;

&lt;p&gt;The important part is the boundary around it.&lt;/p&gt;

&lt;p&gt;We should not automatically retry every 400 or 401. Those usually require a payload, authentication, or configuration fix.&lt;/p&gt;

&lt;p&gt;We should also avoid retrying a non-idempotent operation simply because the network failed.&lt;/p&gt;

&lt;p&gt;For high-volume integrations, a queue is usually easier to operate than synchronous retries inside the web request. Microsoft specifically recommends queueing to flatten traffic spikes against Business Central web services.&lt;/p&gt;

&lt;h1&gt;
  
  
  Step 4: Check the ERP API version before development
&lt;/h1&gt;

&lt;p&gt;The retry design is only useful if the connector targets an API that will remain supported.&lt;/p&gt;

&lt;p&gt;Odoo's current documentation introduces /json/2 for external integrations in version 19. Requests use a model and method in the URL, with named arguments in the JSON body. Authentication uses a bearer API key.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;import os&lt;br&gt;
import requests&lt;/p&gt;

&lt;p&gt;url = "&lt;a href="https://mycompany.example.com/json/2/res.partner/search_read" rel="noopener noreferrer"&gt;https://mycompany.example.com/json/2/res.partner/search_read&lt;/a&gt;"&lt;/p&gt;

&lt;p&gt;headers = {&lt;br&gt;
    "Authorization": f"bearer {os.environ['ODOO_API_KEY']}",&lt;br&gt;
    "Content-Type": "application/json",&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;payload = {&lt;br&gt;
    "domain": [["is_company", "=", True]],&lt;br&gt;
    "fields": ["name"],&lt;br&gt;
    "limit": 10,&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;response = requests.post(url, headers=headers, json=payload, timeout=30)&lt;br&gt;
response.raise_for_status()&lt;/p&gt;

&lt;p&gt;print(response.json())&lt;/p&gt;

&lt;p&gt;The important consulting decision happens before this code.&lt;/p&gt;

&lt;p&gt;If an existing connector still depends on XML-RPC or JSON-RPC, we need to establish its upgrade path. Odoo states that those external RPC APIs are deprecated in version 19 and scheduled for removal in later releases.&lt;/p&gt;

&lt;p&gt;That means an ERP assessment should inspect API dependencies alongside business requirements.&lt;/p&gt;

&lt;p&gt;A connector that works today can still be the wrong implementation choice.&lt;/p&gt;

&lt;h1&gt;
  
  
  Real-World Application: Business Central Scope Before Build
&lt;/h1&gt;

&lt;p&gt;That API and retry trade-off becomes easier to understand when the integration work sits inside a larger ERP implementation.&lt;/p&gt;

&lt;p&gt;We applied this sequencing in a Business Central implementation for a graphics operations business. The project covered finance, inventory, operational workflows, and approximately 62 AL extensions.&lt;/p&gt;

&lt;p&gt;We initially treated the extension list as development scope. The more useful approach was to separate standard Business Central capability from genuine extension requirements before sprint planning.&lt;/p&gt;

&lt;p&gt;We then structured delivery into an 18-week plan with nine two-week sprints. Each sprint could be reviewed against defined functional decisions instead of allowing integration and customization requests to expand the scope informally.&lt;/p&gt;

&lt;p&gt;The quantified outcome available from the implementation scope was therefore an 18-week delivery boundary and approximately 62 extensions.&lt;/p&gt;

&lt;p&gt;The lesson for integration engineers is straightforward. API behavior should be treated as part of ERP architecture, not as a coding detail discovered after functional requirements are approved.&lt;/p&gt;

&lt;p&gt;This is the point where &lt;a href="//oodles.com"&gt;Oodles&lt;/a&gt; treats ERP consulting services differently from a software configuration exercise. The consulting output has to survive contact with development, testing, migration, and operations.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;ERP consulting services should identify integration failure modes before development, especially timeout and retry scenarios.&lt;/li&gt;
&lt;li&gt;Idempotency needs a durable business identifier, not just a retry counter.&lt;/li&gt;
&lt;li&gt;Database uniqueness constraints protect against concurrent duplicate processing when multiple workers consume the same event.&lt;/li&gt;
&lt;li&gt;Retry logic should target transient failures, while permanent client errors should enter an error workflow.&lt;/li&gt;
&lt;li&gt;API version changes belong in ERP technical assessments, because 6. deprecated interfaces can turn a working connector into future migration work.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your ERP scope is growing faster than your delivery plan, discuss your requirements with our &lt;a href="//oodles.com/contact-us"&gt;ERP Consulting Services&lt;/a&gt; team before the next change request becomes development work.&lt;/p&gt;

&lt;h1&gt;
  
  
  What do ERP consulting services include?
&lt;/h1&gt;

&lt;p&gt;ERP consulting services typically cover process discovery, requirements definition, fit-gap analysis, platform evaluation, implementation planning, data migration strategy, integration architecture, testing, training, and post-go-live optimization. The exact scope should depend on whether the organization needs selection, implementation, recovery, or optimization support.&lt;/p&gt;

&lt;h1&gt;
  
  
  How do ERP consultants prevent scope creep?
&lt;/h1&gt;

&lt;p&gt;They establish decision boundaries before development. Requirements are classified by business priority and technical treatment, while integrations, data dependencies, acceptance criteria, and change-control rules are documented early. This makes new requests visible as scope changes rather than allowing them to enter development informally.&lt;/p&gt;

&lt;h1&gt;
  
  
  Should companies customize their ERP?
&lt;/h1&gt;

&lt;p&gt;Customization should address a genuine business or regulatory requirement that standard functionality cannot reasonably support. ERP consulting services should compare customization against configuration, process change, integration, maintenance, and future upgrade costs before recommending an extension.&lt;/p&gt;

&lt;h1&gt;
  
  
  When should data migration planning begin?
&lt;/h1&gt;

&lt;p&gt;ERP consulting services Data migration planning should begin during process discovery, not immediately before go-live. Teams need early decisions about data ownership, required history, cleansing rules, transformation logic, validation, and reconciliation. Those decisions can affect the design of finance, inventory, sales, purchasing, and reporting workflows.&lt;/p&gt;

&lt;h1&gt;
  
  
  How long does ERP consulting take?
&lt;/h1&gt;

&lt;p&gt;The duration depends on the number of processes, entities, ERP platforms, integrations, and migration requirements. A focused discovery can take weeks, while enterprise programs may require months of advisory and implementation planning. The useful measure is decision coverage, not simply elapsed consulting time.&lt;/p&gt;

</description>
      <category>erp</category>
      <category>consultation</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Building Logistics Management Solutions That Keep Inventory Consistent</title>
      <dc:creator>Sanya Mittal</dc:creator>
      <pubDate>Wed, 23 Sep 2026 06:32:35 +0000</pubDate>
      <link>https://dev.to/sanya_mittal_a509a2c50a2d/building-logistics-management-solutions-that-keep-inventory-consistent-27f6</link>
      <guid>https://dev.to/sanya_mittal_a509a2c50a2d/building-logistics-management-solutions-that-keep-inventory-consistent-27f6</guid>
      <description>&lt;p&gt;A shipment can be created twice when inventory reservation happens in two systems. This occurs when an order service updates an ERP while a warehouse service reserves the same stock independently. Logistics Management Solutions must prevent that race before it becomes an operational problem.&lt;/p&gt;

&lt;p&gt;The fix is not another synchronization job. The system needs a clear inventory owner, atomic reservation rules, idempotent APIs, and event-driven updates. This architecture becomes especially important when an organization connects ERP, warehouse management, carrier APIs, and customer portals.&lt;/p&gt;

&lt;p&gt;For teams designing these systems, &lt;a href="https://www.oodles.com/inventory-warehouse-management-/projects/217296?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=backlink&amp;amp;utm_content=devto_article_19" rel="noopener noreferrer"&gt;inventory and warehouse management solutions&lt;/a&gt; provide the operational foundation. The technical question is how to keep inventory state consistent while multiple services act on the same order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context and Setup
&lt;/h2&gt;

&lt;p&gt;The core problem is distributed inventory state. An order service may see 10 units available while another fulfillment request reserves those same units milliseconds earlier.&lt;/p&gt;

&lt;p&gt;This is common in Logistics Management Solutions architectures containing an ERP, warehouse management system (WMS), order management system (OMS), and logistics APIs. Each system may maintain a local representation of stock, but only one should own the authoritative quantity.&lt;/p&gt;

&lt;p&gt;Extensiv's 2024 3PL Warehouse Benchmark Report found that 87% of respondents using a warehouse management system cited real-time inventory tracking and management as a top value proposition.&lt;/p&gt;

&lt;p&gt;That number points to an architectural requirement: inventory visibility must follow inventory transactions, not periodic synchronization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing Logistics Management Solutions Around Reservation Ownership
&lt;/h2&gt;

&lt;p&gt;The safest design starts by separating available Logistics Management Solutions inventory from reserved inventory. The reservation service becomes responsible for changing stock availability, while other services consume the resulting state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Define the Inventory State Machine
&lt;/h3&gt;

&lt;p&gt;Do not store inventory as a single mutable number.&lt;/p&gt;

&lt;p&gt;Use explicit states such as:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;available&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reserved&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;allocated&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;picked&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;shipped&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;returned&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each transition should have one owner and a recorded event.&lt;/p&gt;

&lt;p&gt;For example, an order service can request 4 units. It should not directly decrement warehouse stock. Instead, it calls the reservation service, which validates availability and creates the reservation.&lt;/p&gt;

&lt;p&gt;This prevents different services from silently applying competing inventory rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Make Reservation APIs Idempotent
&lt;/h3&gt;

&lt;p&gt;The second step is preventing retries from creating duplicate reservations.&lt;/p&gt;

&lt;p&gt;A carrier timeout, network failure, or application retry can cause the same request to reach the backend twice. The API should therefore require an idempotency key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/reservations&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Why: repeated requests must return the original reservation.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reservations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Why: the database transaction prevents competing reservations.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reservation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reserveInventory&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reservation&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database transaction matters more than the API endpoint itself. Two concurrent requests must not both read the same available quantity and successfully reserve it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Publish Inventory Events
&lt;/h3&gt;

&lt;p&gt;Once the reservation commits, publish an event such as &lt;code&gt;InventoryReserved&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Downstream systems can consume that event without modifying the inventory owner.&lt;/p&gt;

&lt;p&gt;A typical flow becomes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;OMS submits reservation request.&lt;/li&gt;
&lt;li&gt;Reservation service validates stock.&lt;/li&gt;
&lt;li&gt;Database commits the reservation.&lt;/li&gt;
&lt;li&gt;Event broker publishes &lt;code&gt;InventoryReserved&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;WMS updates fulfillment state.&lt;/li&gt;
&lt;li&gt;Customer portal receives the new availability.&lt;/li&gt;
&lt;li&gt;Analytics consumes the event asynchronously.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This design introduces eventual consistency between services. That is the trade-off.&lt;/p&gt;

&lt;p&gt;A fully synchronous architecture provides stronger immediate consistency but creates tighter dependencies. An event-driven architecture reduces coupling but requires reconciliation, retry handling, and observability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Application
&lt;/h2&gt;

&lt;p&gt;The reservation pattern maps directly to our KLG-ITM Logistics project. The client needed an open-source ERP for logistics operations covering inventory management, order processing, and real-time tracking. We implemented an ERP-centered workflow and integrated logistics processes around shared operational data.&lt;/p&gt;

&lt;p&gt;The public project record confirms the system delivered real-time logistics insights and covered inventory, order processing, and tracking. It does not disclose a numeric performance result.&lt;/p&gt;

&lt;p&gt;[PROJECT METRIC NEEDS INTERNAL VERIFICATION: insert a measured result such as inventory reconciliation time, order-processing time, reservation error rate, or API latency before publication.]&lt;/p&gt;

&lt;p&gt;The architecture also included Java-based ERP applications and control-tower capabilities in the related KLG implementation record.&lt;/p&gt;

&lt;p&gt;This is where &lt;a href="https://www.oodles.com" rel="noopener noreferrer"&gt;Oodles&lt;/a&gt; approaches logistics engineering differently from a simple CRUD implementation. We first establish transaction ownership, then define integrations around those boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Give inventory one authoritative owner before designing integrations.&lt;/li&gt;
&lt;li&gt;Model stock as explicit states instead of repeatedly modifying one quantity field.&lt;/li&gt;
&lt;li&gt;Use idempotency keys for reservation and fulfillment APIs.&lt;/li&gt;
&lt;li&gt;Commit inventory changes before publishing downstream events.&lt;/li&gt;
&lt;li&gt;Accept eventual consistency only when reconciliation and observability are designed with it.&lt;/li&gt;
&lt;li&gt;Measure reservation failures and reconciliation time, not only dashboard availability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are designing or replacing Logistics Management Solutions ERP, WMS, OMS, and carrier integrations, share your architecture or implementation question in the comments. You can also discuss your &lt;a href="https://www.oodles.com/contact-us" rel="noopener noreferrer"&gt;Logistics Management Solutions&lt;/a&gt; requirements with our engineering team.&lt;/p&gt;

&lt;h3&gt;
  
  
  What are Logistics Management Solutions?
&lt;/h3&gt;

&lt;p&gt;Logistics Management Solutions coordinate inventory, orders, warehousing, transportation, tracking, and fulfillment workflows. Technically, they often combine ERP, WMS, OMS, carrier APIs, event processing, and reporting. The architecture should clearly define which system owns each transaction and how other systems receive state changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  How should inventory be synchronized across ERP and WMS?
&lt;/h3&gt;

&lt;p&gt;The safest approach is to assign inventory ownership to one system or service. Other platforms consume controlled updates through APIs or events. Avoid allowing ERP and WMS to independently modify the same available quantity because concurrent updates can create inconsistent stock positions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why are idempotent APIs important in logistics software?
&lt;/h3&gt;

&lt;p&gt;Idempotency prevents repeated requests from producing repeated business transactions. Network failures and client retries are normal in distributed systems. An idempotency key allows the backend to recognize a previously processed reservation, shipment, or fulfillment request and return the original result.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should Logistics Management Solutions platforms use event-driven architecture?
&lt;/h3&gt;

&lt;p&gt;Event-driven architecture works well when several systems need the same operational update. After an inventory reservation commits, the system can publish an event for warehouse, customer, analytics, and notification services. The trade-off is eventual consistency, which requires retries and reconciliation.&lt;/p&gt;

&lt;h3&gt;
  
  
  When should a Logistics Management Solutions use database transactions?
&lt;/h3&gt;

&lt;p&gt;Use database transactions when multiple related changes must succeed or fail together. Inventory reservation is a common example. The transaction should validate available stock and create the reservation atomically. This prevents concurrent requests from reserving the same inventory.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>python</category>
    </item>
  </channel>
</rss>
