<?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>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>
    <item>
      <title>How to Structure Odoo Implementation Services</title>
      <dc:creator>Sanya Mittal</dc:creator>
      <pubDate>Mon, 21 Sep 2026 06:11:47 +0000</pubDate>
      <link>https://dev.to/sanya_mittal_a509a2c50a2d/how-to-structure-odoo-implementation-services-3blm</link>
      <guid>https://dev.to/sanya_mittal_a509a2c50a2d/how-to-structure-odoo-implementation-services-3blm</guid>
      <description>&lt;p&gt;An Odoo deployment can work correctly in a test environment and still create production problems. A common example is an order workflow that creates the expected sales record but fails to synchronize inventory, trigger an external fulfillment API, or enforce a required approval rule.&lt;/p&gt;

&lt;p&gt;This is where Odoo Implementation Services need an engineering approach rather than a configuration-only approach. The implementation has to define module boundaries, data ownership, integration contracts, custom models, security rules, and failure handling before production traffic reaches the system.&lt;/p&gt;

&lt;p&gt;For developers and architects, the useful question is not simply how to configure Odoo. It is how to structure an Odoo system that can be extended without turning every future requirement into another custom patch.&lt;/p&gt;

&lt;p&gt;This guide presents a practical architecture for doing that. For a broader implementation perspective, see &lt;a href="https://www.oodles.com/odoo-implementation/2172802?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=backlink&amp;amp;utm_content=devto_article_15" rel="noopener noreferrer"&gt;Odoo implementation architecture and services&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;A typical Odoo architecture contains the Odoo application layer, PostgreSQL, custom modules, scheduled jobs, external APIs, and sometimes an integration or middleware layer.&lt;/p&gt;

&lt;p&gt;The main engineering constraint is shared business state. A sales transaction can affect inventory, accounting, fulfillment, notifications, and external applications. A change in one workflow can therefore produce side effects across several systems.&lt;/p&gt;

&lt;p&gt;The 2025 Stack Overflow Developer Survey received more than 49,000 responses from developers across 177 countries. It also reported that 84% of respondents were using or planning to use AI tools in their development process, while 46% said they distrust AI output accuracy compared with 33% who trust it.&lt;/p&gt;

&lt;p&gt;For ERP development, the implication is practical: generated code can accelerate implementation, but business rules, database changes, security, and integration behavior still require engineering review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing Odoo Implementation Services as Modules
&lt;/h2&gt;

&lt;p&gt;The solution is to isolate business capabilities and keep customizations explicit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Define the Module Boundary
&lt;/h3&gt;

&lt;p&gt;Start by separating configuration from custom application behavior.&lt;/p&gt;

&lt;p&gt;A useful structure might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;custom_addons/
├── sales_extension/
│   ├── models/
│   ├── views/
│   ├── security/
│   └── data/
├── inventory_extension/
│   ├── models/
│   ├── views/
│   └── security/
└── integration_bridge/
    ├── models/
    ├── services/
    └── data/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each module should have a defined responsibility. For example, an integration module should not contain unrelated inventory rules simply because both workflows happen during order processing.&lt;/p&gt;

&lt;p&gt;This separation makes testing and future changes easier to reason about.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Put Business Rules in the Model Layer
&lt;/h3&gt;

&lt;p&gt;Business rules should be enforced server-side rather than relying only on form-level JavaScript or user-interface restrictions.&lt;/p&gt;

&lt;p&gt;For example:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;odoo&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;odoo.exceptions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ValidationError&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SaleOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;_inherit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sale.order&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;external_reference&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Char&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;action_confirm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Why: prevent external fulfillment without a required reference.
&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;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;external_reference&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;ValidationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;External reference is required before confirmation.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Why: preserve Odoo's standard confirmation workflow.
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;action_confirm&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 part is not the number of lines. The implementation preserves Odoo's existing behavior and adds the additional business constraint at a controlled extension point.&lt;/p&gt;

&lt;p&gt;Unlike replacing the standard workflow entirely, inheritance allows the custom rule to remain close to the original Odoo process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Isolate External Integrations
&lt;/h3&gt;

&lt;p&gt;External API calls should not be scattered across multiple Odoo Implementation Services models.&lt;/p&gt;

&lt;p&gt;A dedicated service layer can centralize authentication, payload construction, retries, logging, and error handling.&lt;/p&gt;

&lt;p&gt;For example:&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;class&lt;/span&gt; &lt;span class="nc"&gt;FulfillmentClient&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;create_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Why: keep external communication outside business models.
&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;self&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Why: fail explicitly instead of silently accepting an invalid response.
&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;400&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;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fulfillment API request 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;return&lt;/span&gt; &lt;span class="n"&gt;response&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For larger integrations, asynchronous processing may be preferable. The Odoo transaction can commit its own business state while a queue worker handles the external request.&lt;/p&gt;

&lt;p&gt;The trade-off is complexity. Synchronous calls are easier to understand, while asynchronous processing provides better isolation for slow or unreliable external services. The right choice depends on whether the external system is required for transaction completion.&lt;/p&gt;

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

&lt;p&gt;In one of our Odoo Implementation Services projects at Oodles, Codeshastra required Odoo implementation, customization, and integration to support its engineering and talent operations.&lt;/p&gt;

&lt;p&gt;The documented solution used Odoo Implementation Services, Python, and open-source technologies, with customized Odoo configuration, integrations, and ongoing support. The project was classified as a Lancer engagement with a team size of up to two resources and a duration of up to three weeks, giving a concrete implementation boundary rather than an open-ended ERP customization program.&lt;/p&gt;

&lt;p&gt;The technical lesson is that implementation scope should be measurable before development begins. Team capacity, delivery window, modules, integrations, and acceptance criteria should all be visible.&lt;/p&gt;

&lt;p&gt;Other Oodles implementation work has involved PostgreSQL and Python alongside Odoo Implementation Services, reinforcing the importance of treating ERP customization as application engineering rather than only UI configuration.&lt;/p&gt;

&lt;p&gt;You can explore more of the engineering and implementation work from &lt;a href="https://www.oodles.com/" rel="noopener noreferrer"&gt;Oodles&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Odoo customizations should be divided into modules with explicit business responsibilities.&lt;/li&gt;
&lt;li&gt;Business rules should be enforced in server-side models rather than only through UI behavior.&lt;/li&gt;
&lt;li&gt;External APIs should have a dedicated integration boundary for authentication, retries, logging, and failures.&lt;/li&gt;
&lt;li&gt;Existing Odoo workflows should be extended where possible instead of unnecessarily replacing them.&lt;/li&gt;
&lt;li&gt;Implementation scope should include measurable limits for resources, timeline, integrations, and acceptance criteria.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are designing an Odoo architecture, extending an existing deployment, or integrating Odoo with external systems, technical questions are welcome in the comments. For implementation discussions, connect with our team about &lt;a href="https://www.oodles.com/contact-us" rel="noopener noreferrer"&gt;Odoo Implementation Services&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What are Odoo Implementation Services?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Odoo Implementation Services cover the technical and functional work required to deploy Odoo for a business, including configuration, custom modules, data migration, integrations, security, testing, deployment, and post-launch support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: When should an Odoo module be customized?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; An Odoo module should be customized when the business requirement cannot reasonably be addressed through standard configuration or an acceptable process change. The customization should have a defined scope, owner, test cases, and upgrade impact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Should Odoo integrations be synchronous or asynchronous?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Synchronous integrations are suitable when the external response is required before the Odoo transaction can continue. Asynchronous processing is preferable when external systems may be slow, unavailable, or capable of processing requests independently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How should Odoo custom code be tested?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Odoo custom code should be tested at the model, workflow, integration, permission, and regression levels. Tests should cover both expected transactions and failure conditions such as invalid data, missing permissions, API errors, and duplicate requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What should developers evaluate during Odoo Implementation Services?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Developers should evaluate module boundaries, database models, security rules, inheritance points, API dependencies, scheduled jobs, data migration, test coverage, deployment procedures, and upgrade impact before approving production changes.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>erp</category>
      <category>webdev</category>
      <category>manufacture</category>
    </item>
    <item>
      <title>Manufacturing ERP Development Company: Where Rollouts Break</title>
      <dc:creator>Sanya Mittal</dc:creator>
      <pubDate>Thu, 17 Sep 2026 06:25:08 +0000</pubDate>
      <link>https://dev.to/sanya_mittal_a509a2c50a2d/manufacturing-erp-development-company-where-rollouts-break-45c7</link>
      <guid>https://dev.to/sanya_mittal_a509a2c50a2d/manufacturing-erp-development-company-where-rollouts-break-45c7</guid>
      <description>&lt;p&gt;A production ERP can pass every planned workflow test and still fail during live operations. CTOs and operations leaders see this when material shortages, rework, substitutions, or order changes disrupt approved processes.&lt;/p&gt;

&lt;p&gt;A Manufacturing ERP Development Company should therefore test operational exceptions before finalizing the system architecture. Our &lt;a href="https://www.oodles.com/erp-domains/7144806?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=backlink&amp;amp;utm_content=devto_article_11" rel="noopener noreferrer"&gt;manufacturing ERP development approach for enterprise operations&lt;/a&gt; starts with transaction dependencies, not screens or module names.&lt;/p&gt;

&lt;p&gt;The pressure to get this right is increasing. Deloitte (2026) reported that 80% of surveyed manufacturing executives plan to allocate at least 20% of improvement budgets to smart manufacturing initiatives. Those investments increasingly depend on connected operational data.&lt;/p&gt;

&lt;p&gt;For a CTO, the risk is architectural. One incorrect inventory state can affect purchasing, production, delivery, costing, and reporting. For an operations leader, the risk appears later as manual corrections and spreadsheet work.&lt;/p&gt;

&lt;p&gt;The better starting point is the exception path. If the ERP handles the unusual production event correctly, the standard workflow usually becomes easier to control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is Happening Now
&lt;/h2&gt;

&lt;p&gt;Manufacturing ERP Development Company use Manufacturing technology investment is moving from isolated applications toward connected operational systems. Deloitte (2026) identifies smart manufacturing, supply chain technology, and artificial intelligence as major areas of manufacturing investment.&lt;/p&gt;

&lt;p&gt;That shift changes what an ERP must accomplish. It can no longer serve only as a recordkeeping system for completed transactions. It increasingly becomes part of the decision chain connecting materials, production, inventory, and customer commitments.&lt;/p&gt;

&lt;p&gt;Deloitte (2025) found that 65% of surveyed manufacturing executives ranked operational risk as their first or second concern for smart manufacturing initiatives. The survey covered 600 executives from large manufacturing organizations.&lt;/p&gt;

&lt;p&gt;This creates an overlooked implementation problem. Teams often validate the process that management designed instead of the process operators actually execute.&lt;/p&gt;

&lt;p&gt;A production manager may approve 100 units. A supplier may deliver material for only 80. The system then needs to represent the shortfall without corrupting reservations, production planning, or customer commitments.&lt;/p&gt;

&lt;p&gt;That is where ERP design becomes an operational discipline rather than a software configuration exercise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Manufacturing ERP Development Company Should Model State Changes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Start with the transaction, not the department
&lt;/h3&gt;

&lt;p&gt;A manufacturing ERP works best when teams map how one transaction changes another.&lt;/p&gt;

&lt;p&gt;Take a production order. Its state can affect material reservations, purchase requirements, work orders, finished goods, quality status, delivery commitments, and accounting entries.&lt;/p&gt;

&lt;p&gt;We map those dependencies before deciding which screens users need. This exposes gaps that separate workshops for purchasing, inventory, and production often miss.&lt;/p&gt;

&lt;p&gt;The useful question is not, “Does the ERP have production management?” The better question is, “What changes everywhere when production quantity changes?”&lt;/p&gt;

&lt;p&gt;Deloitte (2025) found that 35% of surveyed manufacturers ranked advanced production scheduling among their top two manufacturing technology investment priorities. Scheduling therefore needs accurate upstream and downstream transaction data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test the exception path before the happy path
&lt;/h3&gt;

&lt;p&gt;The normal production flow rarely exposes the difficult requirements.&lt;/p&gt;

&lt;p&gt;We test scenarios that force the system to make a state decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A component becomes unavailable after reservation.&lt;/li&gt;
&lt;li&gt;Production completes only part of an order.&lt;/li&gt;
&lt;li&gt;A substitute material enters production.&lt;/li&gt;
&lt;li&gt;Quality inspection sends finished goods into rework.&lt;/li&gt;
&lt;li&gt;A customer changes specifications after production begins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Manufacturing ERP Development Company ensure Each scenario should identify the required approval, inventory movement, audit record, and downstream update.&lt;/p&gt;

&lt;p&gt;This approach also reduces a common development mistake. Teams often build a workaround for an exception before deciding whether that exception deserves a formal business rule.&lt;/p&gt;

&lt;p&gt;Deloitte (2025) reported that manufacturers saw average improvements of 10% to 20% in production output from smart manufacturing initiatives. Those gains depend on reliable operational foundations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separate configuration from genuine customization
&lt;/h3&gt;

&lt;p&gt;A Manufacturing ERP Development Company should not treat every requested change as custom development.&lt;/p&gt;

&lt;p&gt;Manufacturing ERP Development Company first test standard configuration against the business rule. We then assess frequency, financial impact, operational control, upgrade implications, and maintenance effort.&lt;/p&gt;

&lt;p&gt;A frequent production rule may justify customization. A rare exception may work better as a controlled manual approval.&lt;/p&gt;

&lt;p&gt;This distinction matters because customization creates a future maintenance obligation. The initial development effort is only one part of its lifecycle cost.&lt;/p&gt;

&lt;p&gt;Gartner's 2026 conference materials specifically address minimizing ERP customization to reduce schedule and budget overruns. The session covers customization risks across planning, requirements, design, and development.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Oodles Has Seen in Practice
&lt;/h2&gt;

&lt;p&gt;Our Comfold implementation shows why connected workflows matter in manufacturing-oriented ERP projects. Comfold develops space-saving furniture and needed an ERP environment supporting its business operations.&lt;/p&gt;

&lt;p&gt;We developed a custom ERP solution covering four core areas: inventory, sales, purchasing, and production. The implementation connected these functions instead of treating them as isolated departmental applications.&lt;/p&gt;

&lt;p&gt;The specific problem was operational coordination across commercial and manufacturing activities. We addressed it by building the ERP around connected business functions and shared operational information.&lt;/p&gt;

&lt;p&gt;The measurable implementation outcome was scope consolidation across four core operational areas within one ERP environment. The published project record does not disclose a percentage improvement in processing time, cost, or error rate. We therefore do not assign an unsupported performance figure.&lt;/p&gt;

&lt;p&gt;The project also reinforces a useful architecture principle. Manufacturing ERP development should preserve the relationship between demand, purchasing, production, and inventory.&lt;/p&gt;

&lt;p&gt;As manufacturing systems add analytics and artificial intelligence, that relationship becomes more important. Poor transaction structure can propagate bad operational data into every downstream system.&lt;/p&gt;

&lt;p&gt;You can review the broader project context through &lt;a href="https://www.oodles.com/" rel="noopener noreferrer"&gt;Oodles&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Perspective
&lt;/h2&gt;

&lt;p&gt;The next manufacturing ERP project should not begin with a module checklist. It should begin with the moments when production deviates from the approved plan.&lt;/p&gt;

&lt;p&gt;CTOs need to understand how those deviations change system state. Operations leaders need to define who owns each exception and what action follows.&lt;/p&gt;

&lt;p&gt;Manufacturing investment is also moving toward connected data, automation, and artificial intelligence. Deloitte (2026) reports that manufacturers are continuing to invest in these foundations as they pursue greater competitiveness and agility.&lt;/p&gt;

&lt;p&gt;That makes exception handling more than an implementation detail. It becomes part of the data architecture that future automation will depend on.&lt;/p&gt;

&lt;p&gt;If your current ERP scope still describes departments instead of transaction states, that is the point worth resolving before development starts.&lt;/p&gt;

&lt;p&gt;For a workflow review focused on production, inventory, purchasing, and exception handling, start with a &lt;a href="https://www.oodles.com/contact-us/" rel="noopener noreferrer"&gt;Manufacturing ERP Development Company&lt;/a&gt; that can assess the transaction model before the build scope is fixed.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does a Manufacturing ERP Development Company do?
&lt;/h3&gt;

&lt;p&gt;A Manufacturing ERP Development Company designs ERP workflows around production, inventory, purchasing, sales, quality, and related operations. The work can include process mapping, configuration, custom development, integrations, migration, testing, reporting, and deployment.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should manufacturing ERP software handle?
&lt;/h3&gt;

&lt;p&gt;Manufacturing ERP Development Company build Manufacturing ERP software which should connect production planning with materials, purchasing, inventory, quality, sales, and fulfillment. It should also define what happens when planned quantities change, materials become unavailable, or production requires rework.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why do manufacturing ERP projects struggle after development starts?
&lt;/h3&gt;

&lt;p&gt;Projects can struggle when teams validate only the standard workflow. Production exceptions then surface after screens and integrations already exist. Correcting those gaps can affect inventory logic, purchasing rules, reporting, and other connected transactions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should manufacturers customize their ERP?
&lt;/h3&gt;

&lt;p&gt;Manufacturers should customize their ERP when a recurring business rule cannot be handled effectively through configuration. Teams should evaluate frequency, operational impact, maintenance requirements, upgrade effects, and control needs before approving custom development.&lt;/p&gt;

&lt;h3&gt;
  
  
  When should manufacturers involve an ERP development partner?
&lt;/h3&gt;

&lt;p&gt;Manufacturers should involve an ERP development partner before finalizing the technical scope. Early process mapping can expose transaction dependencies, exception states, integration requirements, and customization decisions before they become expensive development changes.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>erp</category>
      <category>webdev</category>
      <category>manufacture</category>
    </item>
    <item>
      <title>OptaPlanner: When Scheduling Models Break in Production</title>
      <dc:creator>Sanya Mittal</dc:creator>
      <pubDate>Mon, 14 Sep 2026 07:50:21 +0000</pubDate>
      <link>https://dev.to/sanya_mittal_a509a2c50a2d/optaplanner-when-scheduling-models-break-in-production-5dl9</link>
      <guid>https://dev.to/sanya_mittal_a509a2c50a2d/optaplanner-when-scheduling-models-break-in-production-5dl9</guid>
      <description>&lt;p&gt;A scheduling engine can produce a mathematically valid schedule and still create operational problems.&lt;/p&gt;

&lt;p&gt;The failure often starts when business rules change faster than the planning model. A technician gains a certification, a customer changes an appointment window, or a vehicle becomes unavailable.&lt;/p&gt;

&lt;p&gt;This is where It implementations need more than solver configuration. They need a planning model that can absorb changing constraints without turning every operational change into a development task.&lt;/p&gt;

&lt;p&gt;It was designed for problems such as employee rostering, vehicle routing, timetabling, and job-shop scheduling. Its constraint-based approach evaluates hard and soft constraints to find better feasible solutions.&lt;/p&gt;

&lt;p&gt;For organizations 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_11" rel="noopener noreferrer"&gt;how OptaPlanner works in enterprise systems&lt;/a&gt;, the important question is therefore not only whether the solver can find a schedule. It is whether the surrounding application can keep that schedule useful when reality changes.&lt;/p&gt;

&lt;p&gt;That distinction matters because Red Hat announced OptaPlanner's end of life in 2024. Timefold, created by the original their team, now provides the actively maintained successor path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Scheduling Problems Keep Happening
&lt;/h2&gt;

&lt;p&gt;Most production scheduling failures begin outside the solver.&lt;/p&gt;

&lt;p&gt;A planning model may represent employees, vehicles, locations, and time windows correctly. Problems emerge when operational rules remain hidden in spreadsheets, dispatcher decisions, or application code.&lt;/p&gt;

&lt;p&gt;This creates three separate sources of truth.&lt;/p&gt;

&lt;p&gt;The solver sees constraints. The application sees transactions. Operations teams see exceptions. When those three views diverge, the generated schedule may satisfy the model but fail the operation.&lt;/p&gt;

&lt;p&gt;Gartner identified technical incompatibility as one of the biggest challenges for technology adoption in operations in its 2024 research. Gartner also found that 56% of surveyed infrastructure and operations technologies were already in deployment phases.&lt;/p&gt;

&lt;p&gt;The pattern is important for CTOs. Scaling a planning system is not simply a matter of increasing solver capacity. The integration model, constraint lifecycle, and exception workflow must scale together.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Framework for Production Scheduling
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Model the business constraint before tuning the solver
&lt;/h3&gt;

&lt;p&gt;We start by separating hard constraints from soft constraints.&lt;/p&gt;

&lt;p&gt;A hard constraint might prevent assigning an unqualified technician to a regulated job. A soft constraint might favor a shorter travel distance.&lt;/p&gt;

&lt;p&gt;That distinction gives operations leaders control over trade-offs. It also makes later rule changes easier to test.&lt;/p&gt;

&lt;p&gt;It's documentation describes this approach across use cases such as employee rostering and vehicle routing.&lt;/p&gt;

&lt;p&gt;The practical test is simple: every important scheduling decision should have an identifiable business rule behind it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Treat schedule changes as production events
&lt;/h3&gt;

&lt;p&gt;A schedule rarely remains static for an entire operating day.&lt;/p&gt;

&lt;p&gt;New orders arrive. Employees call in sick. Vehicles break down. Customers move appointment windows.&lt;/p&gt;

&lt;p&gt;An application therefore needs an event-driven update path rather than repeatedly rebuilding the entire planning problem.&lt;/p&gt;

&lt;p&gt;This is especially relevant for field service and logistics systems. McKinsey documented a smart scheduling implementation where false truck rolls fell by 80%. Field-worker productivity increased by 20% to 30%, while scheduler productivity increased by 10% to 20%.&lt;/p&gt;

&lt;p&gt;The lesson is not that every implementation will achieve those numbers. The lesson is that measurable operational outcomes should guide solver design.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Decide when to optimize and when to migrate
&lt;/h3&gt;

&lt;p&gt;In this projects also face a technology lifecycle decision.&lt;/p&gt;

&lt;p&gt;Red Hat announced it's end of life in spring 2024. Timefold Solver 1.x provides the successor path, with Java 17 as the minimum supported version and compatibility with newer Spring Boot and Quarkus generations.&lt;/p&gt;

&lt;p&gt;That creates three practical choices.&lt;/p&gt;

&lt;p&gt;Keep an existing timefold deployment when its environment remains supported internally. Migrate when dependency risk or platform modernization justifies the change. Redesign when the planning model itself no longer represents the operation.&lt;/p&gt;

&lt;p&gt;Migration should therefore begin with dependency and constraint analysis, not a simple package replacement.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Learned from a Real Implementation
&lt;/h2&gt;

&lt;p&gt;One relevant Oodles implementation was an enterprise planning and optimization platform for Thales.&lt;/p&gt;

&lt;p&gt;The project used it with Spring Boot and Angular. The core requirement was to support planning and optimization inside an enterprise application while integrating with existing systems.&lt;/p&gt;

&lt;p&gt;We structured the solution around an API-driven application layer and a dedicated optimization layer. The architecture separated enterprise workflows from solver logic, which made planning rules easier to evolve without coupling every business transaction to the solver.&lt;/p&gt;

&lt;p&gt;The project included secure deployment, extensible APIs, and integration with existing enterprise systems.&lt;/p&gt;

&lt;p&gt;We would not publish an invented percentage here. For an implementation article, the correct metric should come from the project's delivery records.&lt;/p&gt;

&lt;p&gt;This architecture also informs newer planning work. In workforce and route optimization projects, we have applied similar principles around skills, availability, location, service windows, and operational constraints.&lt;/p&gt;

&lt;p&gt;The key design decision is to keep the solver responsible for optimization while the application remains responsible for operational truth.&lt;/p&gt;

&lt;p&gt;For organizations reviewing their planning architecture, &lt;a href="//oodles.com"&gt;Oodles&lt;/a&gt; provides engineering experience across route optimization, workforce scheduling, and enterprise planning systems.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Model hard and soft constraints separately before tuning solver performance.&lt;/li&gt;
&lt;li&gt;Treat operational changes as events that can update planning problems.&lt;/li&gt;
&lt;li&gt;Keep business transactions separate from optimization logic.&lt;/li&gt;
&lt;li&gt;Measure scheduling outcomes using operational metrics, not solver scores alone.&lt;/li&gt;
&lt;li&gt;Review it's dependencies before committing to long-term maintenance.&lt;/li&gt;
&lt;li&gt;Consider Timefold when the existing environment creates lifecycle or platform risk.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your planning model is becoming difficult to maintain, discuss your &lt;a href="//oodles.com/contact-us"&gt;OptaPlanner&lt;/a&gt; requirements with our engineering team.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is OptaPlanner still supported?
&lt;/h3&gt;

&lt;p&gt;Red Hat announced OptaPlanner's end of life in 2024. Timefold Solver now provides the actively maintained continuation from the origin their team. Existing projects should review dependencies, Java versions, APIs, and deployment requirements before deciding whether to migrate.&lt;/p&gt;

&lt;h3&gt;
  
  
  What problems can OptaPlanner solve?
&lt;/h3&gt;

&lt;p&gt;It supports constraint-based planning problems including employee rostering, vehicle routing, timetabling, appointment scheduling, and job-shop scheduling. The strongest use cases involve multiple competing constraints where finding a feasible and better allocation manually becomes difficult.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should companies migrate from OptaPlanner to Timefold?
&lt;/h3&gt;

&lt;p&gt;Migration depends on the existing application's dependencies and lifecycle requirements. Timefold Solver 1.x supports Java 17 and newer frameworks. Teams should first inventory APIs, constraints, persistence, build dependencies, and framework versions before changing the solver.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can OptaPlanner handle real-time scheduling?
&lt;/h3&gt;

&lt;p&gt;It can support planning applications where schedules change during operations. The application architecture still needs to manage incoming events, update planning facts, and decide when to trigger optimization. Real-time behavior therefore depends on the surrounding system as much as the solver.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is OptaPlanner better than manual scheduling?
&lt;/h3&gt;

&lt;p&gt;It can be when many constraints interact across people, assets, locations, and time windows. The business case should be measured against planning time, schedule quality, travel, utilization, exceptions, and manual intervention rather than solver performance alone.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>timefold</category>
      <category>opensource</category>
      <category>automation</category>
    </item>
    <item>
      <title>Designing ERP Development Services for Scale</title>
      <dc:creator>Sanya Mittal</dc:creator>
      <pubDate>Thu, 10 Sep 2026 06:08:53 +0000</pubDate>
      <link>https://dev.to/sanya_mittal_a509a2c50a2d/designing-erp-development-services-for-scale-c9n</link>
      <guid>https://dev.to/sanya_mittal_a509a2c50a2d/designing-erp-development-services-for-scale-c9n</guid>
      <description>&lt;p&gt;An ERP can work perfectly in staging and still become difficult to maintain once transaction volume, integrations, and custom workflows increase. A common failure point is not the ERP platform itself. It is tightly coupled business logic, unclear ownership between modules, and integrations that directly modify core ERP data.&lt;/p&gt;

&lt;p&gt;This is where ERP Development Services require an engineering-first approach.&lt;/p&gt;

&lt;p&gt;For architects and development teams, the objective should be to build an ERP that can evolve without turning every new requirement into a core-code change. That means defining module boundaries, API contracts, data ownership, background jobs, observability, and deployment practices before adding customization.&lt;/p&gt;

&lt;p&gt;This article presents a practical architecture for &lt;a href="https://www.oodles.com/custom-erp/11?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=backlink&amp;amp;utm_content=devto_article_09" rel="noopener noreferrer"&gt;ERP development services for custom enterprise systems&lt;/a&gt; and explains how Oodles approaches these decisions in real implementations.&lt;/p&gt;

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

&lt;p&gt;A modern ERP rarely operates as an isolated application. It typically exchanges data with commerce platforms, CRM systems, payment providers, warehouses, marketplaces, payroll systems, and external databases.&lt;/p&gt;

&lt;p&gt;A useful baseline architecture 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;Client Apps
    |
    v
API / Integration Layer
    |
    v
ERP Application
 |       |       |
Sales  Inventory Finance
    \      |      /
      PostgreSQL
           |
      Background Jobs
           |
    External Systems
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key design principle is that not every system should directly manipulate every ERP table.&lt;/p&gt;

&lt;p&gt;The 2024 Stack Overflow Developer Survey found that 49% of developers use PostgreSQL, making it the most-used database in that year's survey. The same survey reported Docker usage among 59% of professional developers. These figures do not prove that PostgreSQL or Docker is automatically right for every ERP, but they illustrate why familiar database and deployment patterns matter when building teams and infrastructure around enterprise applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  ERP Development Services: Build Around Boundaries
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Define transaction ownership
&lt;/h3&gt;

&lt;p&gt;The first architectural step is to identify which component owns each business transaction.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;The commerce platform owns the customer checkout.&lt;/li&gt;
&lt;li&gt;The ERP owns the sales order after synchronization.&lt;/li&gt;
&lt;li&gt;The warehouse system owns physical picking activity.&lt;/li&gt;
&lt;li&gt;The ERP receives fulfilment status.&lt;/li&gt;
&lt;li&gt;Finance owns invoice and payment reconciliation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This prevents two systems from becoming competing sources of truth.&lt;/p&gt;

&lt;p&gt;A common alternative is direct database synchronization between applications. That can be simpler initially, but it creates hidden coupling. An API or event-based boundary makes ownership explicit and gives engineering teams a controlled integration contract.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Keep custom logic outside core paths
&lt;/h3&gt;

&lt;p&gt;The second step in ERP Development Services is to isolate business-specific logic wherever the platform architecture permits it.&lt;/p&gt;

&lt;p&gt;For example, an order-processing service might validate an incoming request before creating an ERP 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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_sales_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;erp_client&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Why: validate before writing to ERP to prevent partial transactions.
&lt;/span&gt;    &lt;span class="nf"&gt;validate_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="c1"&gt;# Why: keep ERP-specific implementation behind an API client.
&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;erp_client&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;customer_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_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;lines&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lines&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="c1"&gt;# Why: return a stable application response instead of exposing ERP internals.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_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;response&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;status&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;created&lt;/span&gt;&lt;span class="sh"&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 part is not the language or framework. It is the boundary.&lt;/p&gt;

&lt;p&gt;The application does not need to know how the ERP stores every field. It needs a stable contract for creating an order.&lt;/p&gt;

&lt;p&gt;This approach also makes automated testing easier because the ERP Development Services client can be mocked independently from business rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Design for asynchronous work
&lt;/h3&gt;

&lt;p&gt;Not every ERP operation should block the user's request.&lt;/p&gt;

&lt;p&gt;Reports, bulk imports, notification delivery, inventory synchronization, document generation, and external API retries are good candidates for background processing.&lt;/p&gt;

&lt;p&gt;A typical pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Request
     |
     v
Create Job
     |
     v
Queue
     |
     +---- Worker A -&amp;gt; ERP
     |
     +---- Worker B -&amp;gt; External API
     |
     +---- Worker C -&amp;gt; Notification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trade-off is additional infrastructure and operational complexity.&lt;/p&gt;

&lt;p&gt;Synchronous processing is easier to reason about for small transactions. Asynchronous processing becomes useful when external dependencies are slow, workloads are variable, or operations can be safely retried.&lt;/p&gt;

&lt;p&gt;The design should therefore be based on transaction characteristics rather than adopting queues simply because the architecture looks more advanced.&lt;/p&gt;

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

&lt;p&gt;In one of our ERP Development Services projects at Oodles, My Mandi required an inventory management ERP for a B2B2C marketplace along with a mobile application.&lt;/p&gt;

&lt;p&gt;Oodles developed a customized Odoo ERP and a Flutter mobile application, using Python for backend development and DevOps practices for integration and deployment. The implementation connected inventory management with the marketplace experience instead of treating the ERP as an isolated administrative system.&lt;/p&gt;

&lt;p&gt;The measurable outcome documented by the client was that more than 200 members were brought onto one platform, while users gained better reporting visibility and the ability to book orders through the application.&lt;/p&gt;

&lt;p&gt;The architecture lesson is important: the ERP was part of an application ecosystem, not simply a back-office database.&lt;/p&gt;

&lt;p&gt;For another implementation, Oodles built Genie as a modular ERP covering production, inventory, sales, HR, finance, and marketing. The implementation included production planning, QR-based inventory tracking, custom sales workflows, dashboards, finance and HR integrations, and marketing automation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://oodles.com/" rel="noopener noreferrer"&gt;Oodles&lt;/a&gt; approaches these implementations by combining ERP configuration with custom development, integrations, APIs, and application architecture rather than treating customization as isolated feature work.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Define ownership for every critical business transaction before designing integrations.&lt;/li&gt;
&lt;li&gt;Keep business rules behind application or service boundaries instead of spreading them across database operations.&lt;/li&gt;
&lt;li&gt;Use stable API contracts so external systems do not depend on internal ERP implementation details.&lt;/li&gt;
&lt;li&gt;Move long-running and retryable workloads to background processing when synchronous execution creates operational bottlenecks.&lt;/li&gt;
&lt;li&gt;Treat the ERP as one component within a wider application ecosystem when commerce, mobile, CRM, warehouse, or finance systems are involved.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Have a specific ERP architecture, integration boundary, or customization problem you are evaluating? Share the technical context in the comments, or discuss your &lt;a href="https://www.oodles.com/contact-us/" rel="noopener noreferrer"&gt;ERP Development Services&lt;/a&gt; requirements with the Oodles engineering team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What does ERP development involve beyond configuration?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; ERP Development Services can include custom modules, business logic, APIs, integrations, database work, workflow automation, background processing, reporting, testing, deployment, and application-specific interfaces. Configuration changes platform behavior without necessarily requiring custom code, while development extends the platform for requirements it cannot support natively.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Should ERP integrations use APIs or direct database access?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; APIs are generally preferable when they provide the required capabilities because they establish an explicit contract between systems. Direct database access can introduce schema coupling and make upgrades harder. Database-level integration may still be appropriate for controlled internal workloads where ownership and compatibility are clearly defined.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: When should ERP processing become asynchronous?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; ERP processing should become asynchronous when an operation is long-running, retryable, externally dependent, or capable of creating unpredictable request latency. Bulk imports, report generation, notifications, and external synchronization are common examples. User-facing transactions that require immediate confirmation should generally remain synchronous.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do ERP Development Services support scalability?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; &lt;strong&gt;ERP Development Services&lt;/strong&gt; support scalability by separating modules, defining transaction ownership, controlling integration boundaries, optimizing database access, introducing background workers where appropriate, and designing deployment environments that can scale independently. Scalability should be tested against actual transaction patterns rather than assumed from infrastructure specifications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is Odoo suitable for custom ERP development?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Odoo can support custom ERP development when its modular architecture matches the organization's requirements. Oodles has implemented Odoo solutions involving custom modules, inventory, manufacturing, sales, finance, mobile applications, integrations, and workflow automation.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building Vehicle Route Optimization Software with Timefold</title>
      <dc:creator>Sanya Mittal</dc:creator>
      <pubDate>Wed, 09 Sep 2026 06:19:37 +0000</pubDate>
      <link>https://dev.to/sanya_mittal_a509a2c50a2d/building-vehicle-route-optimization-software-with-timefold-3536</link>
      <guid>https://dev.to/sanya_mittal_a509a2c50a2d/building-vehicle-route-optimization-software-with-timefold-3536</guid>
      <description>&lt;p&gt;A Vehicle Route Optimization Software can return a valid route and still produce a poor operational plan. This happens when the engine optimizes distance without understanding driver hours, vehicle capacity, service windows, priorities, or changing assignments.&lt;/p&gt;

&lt;p&gt;Vehicle Route Optimization Software solves the broader problem by combining routing data with business constraints and optimization rules. For developers and solution architects, the key challenge is designing an architecture where those constraints can be evaluated repeatedly without turning the routing API into a bottleneck.&lt;/p&gt;

&lt;p&gt;This article walks through a practical Vehicle Route Optimization Software architecture using Java, Spring Boot, Timefold, and a distance-matrix provider. It focuses on constraint modeling, API boundaries, replanning, and performance measurement. Oodles has applied similar planning patterns in logistics and vehicle-routing implementations. You can see the approach in &lt;a href="https://www.oodles.com/planning-solutions-/2004207/case-study/optimize-vehicle-routing-with-oodles-erp-smarter-fleet-management?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=backlink&amp;amp;utm_content=devto_article_08" rel="noopener noreferrer"&gt;how vehicle routing optimization is implemented with ERP and planning systems&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;A practical architecture separates routing, optimization, and application state.&lt;/p&gt;

&lt;p&gt;A typical flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Orders / Jobs
     |
     v
Spring Boot API
     |
     +----&amp;gt; Validation &amp;amp; Constraint Model
     |
     +----&amp;gt; Distance Matrix / Map Provider
     |
     v
Timefold Solver
     |
     v
Optimized Assignments
     |
     v
PostgreSQL / ERP / Driver App
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AWS's reference architecture for intelligent route optimization follows a similar separation. It combines order data, location services, route matrices, persistent itinerary data, and near-real-time vehicle tracking. AWS also documents single-digit millisecond performance for DynamoDB itinerary queries in its reference implementation.&lt;/p&gt;

&lt;p&gt;The important architectural boundary is this: the map provider calculates travel information, while the optimization engine decides how that information should be used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing Vehicle Route Optimization Software
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Model the planning problem
&lt;/h3&gt;

&lt;p&gt;Start with domain objects rather than solver configuration.&lt;/p&gt;

&lt;p&gt;For example:&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;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Delivery&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Location&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;serviceMinutes&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;LocalDateTime&lt;/span&gt; &lt;span class="n"&gt;windowStart&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;LocalDateTime&lt;/span&gt; &lt;span class="n"&gt;windowEnd&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Vehicle&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;LocalTime&lt;/span&gt; &lt;span class="n"&gt;shiftStart&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;LocalTime&lt;/span&gt; &lt;span class="n"&gt;shiftEnd&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The solver then works with planning entities and planning variables.&lt;/p&gt;

&lt;p&gt;A useful model normally includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vehicles&lt;/li&gt;
&lt;li&gt;Delivery stops&lt;/li&gt;
&lt;li&gt;Driver availability&lt;/li&gt;
&lt;li&gt;Vehicle capacity&lt;/li&gt;
&lt;li&gt;Delivery windows&lt;/li&gt;
&lt;li&gt;Service duration&lt;/li&gt;
&lt;li&gt;Depot locations&lt;/li&gt;
&lt;li&gt;Route sequence&lt;/li&gt;
&lt;li&gt;Travel time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The distinction matters because a routing engine cannot optimize a constraint that has never been represented in the domain model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Build constraints instead of hard-coding routes
&lt;/h3&gt;

&lt;p&gt;Timefold supports constraint-based planning, allowing developers to separate business rules from application code.&lt;/p&gt;

&lt;p&gt;For example:&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="nc"&gt;Constraint&lt;/span&gt; &lt;span class="nf"&gt;vehicleCapacity&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;RouteStop&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;filter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getVehicle&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getUsedCapacity&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;stop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getVehicle&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getCapacity&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="c1"&gt;// Why: capacity violations must never be accepted.&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A second constraint can handle delivery windows:&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="nc"&gt;Constraint&lt;/span&gt; &lt;span class="nf"&gt;deliveryWindow&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;RouteStop&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;filter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;RouteStop:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;isOutsideDeliveryWindow&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;// Why: late deliveries are operationally more serious&lt;/span&gt;
        &lt;span class="c1"&gt;// than a small increase in driving distance.&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This hard/soft distinction is critical.&lt;/p&gt;

&lt;p&gt;Capacity violations may be hard constraints, while unnecessary travel distance may be a soft constraint. That allows the solver to choose a slightly longer route when doing so avoids a delivery-window violation.&lt;/p&gt;

&lt;p&gt;Oodles' planning work uses OptaPlanner and Timefold for vehicle routing, scheduling, resource allocation, and constraint-based planning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Separate optimization from route calculation
&lt;/h3&gt;

&lt;p&gt;Do not make the optimization service responsible for every geographic calculation.&lt;/p&gt;

&lt;p&gt;A better pattern is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Receive delivery and vehicle data.&lt;/li&gt;
&lt;li&gt;Validate business constraints.&lt;/li&gt;
&lt;li&gt;Request travel-time or distance data.&lt;/li&gt;
&lt;li&gt;Build the planning problem.&lt;/li&gt;
&lt;li&gt;Run the solver.&lt;/li&gt;
&lt;li&gt;Persist the selected assignments.&lt;/li&gt;
&lt;li&gt;Return an immutable route plan.&lt;/li&gt;
&lt;li&gt;Trigger replanning when material conditions change.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Amazon Location Service, for example, provides route calculations and waypoint optimization, while an application can use those results inside a wider planning workflow.&lt;/p&gt;

&lt;p&gt;This separation also makes provider changes easier. The optimization layer should consume normalized travel data instead of depending directly on a particular mapping API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Replanning Without Creating Instability
&lt;/h2&gt;

&lt;p&gt;A route should not be recalculated every time a GPS coordinate changes.&lt;/p&gt;

&lt;p&gt;Instead, define business events that justify replanning:&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="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newOrder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isHighPriority&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;vehicle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isUnavailable&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;deliveryWindowChanged&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Why: replan only when the current solution may no longer&lt;/span&gt;
    &lt;span class="c1"&gt;// satisfy important operational constraints.&lt;/span&gt;
    &lt;span class="n"&gt;solverManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;solve&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;problemId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;problem&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typical triggers include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New high-priority order&lt;/li&gt;
&lt;li&gt;Vehicle breakdown&lt;/li&gt;
&lt;li&gt;Driver availability change&lt;/li&gt;
&lt;li&gt;Delivery cancellation&lt;/li&gt;
&lt;li&gt;Major delay&lt;/li&gt;
&lt;li&gt;Changed service window&lt;/li&gt;
&lt;li&gt;Capacity change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This avoids unnecessary solver executions and reduces route churn for drivers.&lt;/p&gt;

&lt;p&gt;The trade-off is important. More frequent optimization can react faster, but excessive replanning can create operational instability. The correct threshold depends on how volatile the workload is.&lt;/p&gt;

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

&lt;p&gt;In one of our Vehicle Route Optimization Software projects at Oodles, a client needed to address vehicle routing and scheduling using OptaPlanner. The solution was implemented with Java and Spring Boot, with optimization logic focused on vehicle assignment, route sequencing, and scheduling.&lt;/p&gt;

&lt;p&gt;Oodles also documented a logistics platform for Navntrack that combined fleet tracking, mobile workforce management, asset monitoring, IoT devices, and Timefold-based planning.&lt;/p&gt;

&lt;p&gt;The measurable engineering outputs in these systems include route distance, travel time, delivery-window compliance, vehicle utilization, route completion time, and replanning frequency. Those metrics should be captured before and after optimization rather than relying on subjective claims about efficiency.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.oodles.com/" rel="noopener noreferrer"&gt;Oodles&lt;/a&gt; uses this planning-engine approach when routing needs to connect with broader ERP, workforce, logistics, or operational workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Model business constraints before selecting or tuning a solver.&lt;/li&gt;
&lt;li&gt;Keep map calculations separate from optimization logic.&lt;/li&gt;
&lt;li&gt;Use hard constraints for rules that cannot be violated and soft constraints for competing preferences.&lt;/li&gt;
&lt;li&gt;Trigger replanning from meaningful operational events rather than every telemetry update.&lt;/li&gt;
&lt;li&gt;Measure route distance, travel time, constraint violations, utilization, and replanning frequency to evaluate the system objectively.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Have questions about solver architecture, constraint modeling, or integrating Vehicle Route Optimization Software with an ERP or logistics platform? &lt;a href="https://www.oodles.com/contact-us" rel="noopener noreferrer"&gt;Discuss your routing architecture with our technical team&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is Vehicle Route Optimization Software?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Vehicle Route Optimization Software uses algorithms to assign stops and sequence routes while considering constraints such as capacity, delivery windows, driver availability, travel time, and operational priorities. It differs from basic navigation because the objective is an executable business plan, not simply the shortest path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why use Timefold for vehicle routing?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Timefold provides Vehicle Route Optimization Software constraint-based planning capabilities that let developers represent hard and soft business rules. This is useful when routing involves capacity, schedules, skills, time windows, or resource availability that cannot be represented by distance optimization alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Should route optimization run synchronously in an API request?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Usually not for complex planning problems. A synchronous request can hold connections while the solver searches for a solution. An asynchronous job model lets the API submit a planning problem, track solver status, and retrieve the resulting route independently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How should route optimization performance be measured?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Measure solver duration, route distance, travel time, constraint violations, route completion time, vehicle utilization, and replanning frequency. Tracking these metrics separately helps engineers identify whether a performance problem comes from data preparation, distance calculation, or the solver itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can Vehicle Route Optimization Software integrate with an ERP?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Yes. An ERP can provide orders, customer priorities, inventory requirements, vehicle information, and scheduling data to the optimization service. The resulting routes can then be returned to the ERP, dispatcher interface, or driver application through APIs or event-driven integration.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>timefold</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building Transportation Management Solutions with Event-Driven ERP Integration</title>
      <dc:creator>Sanya Mittal</dc:creator>
      <pubDate>Mon, 07 Sep 2026 07:06:46 +0000</pubDate>
      <link>https://dev.to/sanya_mittal_a509a2c50a2d/building-transportation-management-solutions-with-event-driven-erp-integration-5fff</link>
      <guid>https://dev.to/sanya_mittal_a509a2c50a2d/building-transportation-management-solutions-with-event-driven-erp-integration-5fff</guid>
      <description>&lt;p&gt;A common Transportation Management Solutions integration failure starts with a simple assumption: the Transportation Management Solutions can be connected to the ERP through a few REST APIs and the problem is solved.&lt;/p&gt;

&lt;p&gt;In production, shipment events arrive asynchronously, carriers use different status codes, warehouse systems update at different times, and ERP transactions often require ordering and validation. This is where Transportation Management Solutions need an integration architecture rather than a collection of API calls.&lt;/p&gt;

&lt;p&gt;For developers building &lt;a href="https://www.oodles.com/inventory-warehouse-management-/2172960?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=backlink&amp;amp;utm_content=devto_article_07" rel="noopener noreferrer"&gt;enterprise inventory and transportation workflows&lt;/a&gt;, the architectural challenge is connecting orders, shipments, carriers, warehouses, inventory, and financial records without creating tightly coupled services.&lt;/p&gt;

&lt;p&gt;A practical approach is to treat shipment changes as domain events and allow each downstream system to react according to its responsibility.&lt;/p&gt;

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

&lt;p&gt;A transportation platform typically sits between several systems:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    ┌──────────────┐
                    │     ERP      │
                    └──────┬───────┘
                           │
                    Order / Invoice
                           │
                    ┌──────▼───────┐
                    │ Integration  │
                    │   Service    │
                    └──────┬───────┘
                           │
              ┌────────────┼────────────┐
              │            │            │
        ┌─────▼─────┐ ┌────▼────┐ ┌────▼─────┐
        │    TMS    │ │Warehouse│ │ Carrier  │
        └───────────┘ └─────────┘ └──────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The integration layer should normalize external events before they reach business services.&lt;/p&gt;

&lt;p&gt;For example, one carrier may send &lt;code&gt;OUT_FOR_DELIVERY&lt;/code&gt;, another may send &lt;code&gt;OFD&lt;/code&gt;, and an internal warehouse system may call the same state &lt;code&gt;DISPATCHED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A canonical internal event avoids spreading carrier-specific logic throughout the application.&lt;/p&gt;

&lt;p&gt;This matters as shipment volume grows. The 2024 Stack Overflow Developer Survey reported that PostgreSQL was used by 48.7% of all respondents, making it the most popular database in the survey's database category. PostgreSQL is therefore a practical choice for systems that need transactional storage alongside integration workloads, although database selection should still follow workload requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing Transportation Management Solutions Around Events
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Define a Canonical Shipment Model
&lt;/h3&gt;

&lt;p&gt;The first step is to establish a common representation of shipment state.&lt;/p&gt;

&lt;p&gt;The model should contain only information that downstream services actually need:&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;"shipmentId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SHP-10482"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-7821"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"carrier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"carrier-a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OUT_FOR_DELIVERY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eventTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-07T08:30:00Z"&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 important design decision is separating the external carrier status from the internal business status.&lt;/p&gt;

&lt;p&gt;This allows the adapter for each carrier to translate its API into the same domain model.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Carrier A → ERP
Carrier B → ERP
Carrier C → ERP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Carrier A ─┐
Carrier B ─┼→ Normalized Event → Business Services
Carrier C ─┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That reduces coupling and makes adding another carrier less disruptive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Publish Shipment Events
&lt;/h3&gt;

&lt;p&gt;Once an event is normalized, publish it to a message broker such as Kafka, RabbitMQ, or a cloud-native queue.&lt;/p&gt;

&lt;p&gt;A simplified Node.js example might look like this:&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;handleCarrierUpdate&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="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Why: convert carrier-specific statuses before business processing.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeCarrierEvent&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: publishing asynchronously prevents carrier callbacks&lt;/span&gt;
  &lt;span class="c1"&gt;// from waiting for every downstream ERP operation.&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;eventBus&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shipment.status.changed&lt;/span&gt;&lt;span class="dl"&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="k"&gt;return&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 consumer can then update the relevant systems independently:&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;processShipmentEvent&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="c1"&gt;// Why: idempotency prevents duplicate carrier callbacks&lt;/span&gt;
  &lt;span class="c1"&gt;// from creating duplicate ERP transactions.&lt;/span&gt;
  &lt;span class="k"&gt;if &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;eventStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&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;eventId&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;eventStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&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;eventId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Update transportation state.&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;shipmentService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateStatus&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="c1"&gt;// Notify ERP only when the business state requires it.&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DELIVERED&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;erpService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recordDelivery&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;eventId&lt;/code&gt; is important. Carrier APIs can retry callbacks, and network failures can make the same event appear more than once.&lt;/p&gt;

&lt;p&gt;Idempotency turns repeated delivery into a manageable condition instead of a duplicate-order or duplicate-invoice problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Decide Where Orchestration Belongs
&lt;/h3&gt;

&lt;p&gt;Not every workflow should be event-driven.&lt;/p&gt;

&lt;p&gt;Use synchronous APIs when a caller needs an immediate response, such as checking whether a shipment can be created.&lt;/p&gt;

&lt;p&gt;Use asynchronous events when processing can happen independently, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Delivery status updates&lt;/li&gt;
&lt;li&gt;Carrier tracking events&lt;/li&gt;
&lt;li&gt;Warehouse notifications&lt;/li&gt;
&lt;li&gt;Freight reconciliation&lt;/li&gt;
&lt;li&gt;Customer notifications&lt;/li&gt;
&lt;li&gt;Analytics pipelines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-off is complexity. An event-driven architecture improves isolation and retry handling, but it also introduces eventual consistency, message ordering concerns, monitoring requirements, and dead-letter handling.&lt;/p&gt;

&lt;p&gt;The correct architecture depends on business criticality, not technology preference.&lt;/p&gt;

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

&lt;p&gt;In one of our logistics-related implementations at Oodles, the client operated promotional graphics workflows involving up to 41,000 parts per job. The environment included separate systems for quoting, fulfillment, and finance, with Business Central selected as the ERP foundation.&lt;/p&gt;

&lt;p&gt;The technical challenge was not simply creating another operational application. The solution needed to connect fulfillment workflows with ERP processes while preserving system responsibilities.&lt;/p&gt;

&lt;p&gt;The approach centered on Business Central as the ERP foundation and defined integration boundaries between operational workflows and financial processes.&lt;/p&gt;

&lt;p&gt;The measurable scale requirement was significant: kit planning could involve 41,000 parts for a single job. That made data synchronization, process ownership, and transaction design important architectural considerations.&lt;/p&gt;

&lt;p&gt;The broader lesson applies directly to Transportation Management Solutions: integration should be designed around business entities and events rather than individual screens or vendor APIs.&lt;/p&gt;

&lt;p&gt;You can explore more about &lt;a href="https://www.oodles.com/" rel="noopener noreferrer"&gt;Oodles&lt;/a&gt; and its engineering capabilities separately from the architecture itself.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Normalize carrier events before sending transportation data into ERP or business services.&lt;/li&gt;
&lt;li&gt;Use idempotency keys for shipment callbacks because external systems can retry events.&lt;/li&gt;
&lt;li&gt;Keep carrier-specific status mapping inside integration adapters rather than business logic.&lt;/li&gt;
&lt;li&gt;Use synchronous APIs for immediate decisions and asynchronous events for independent workflows.&lt;/li&gt;
&lt;li&gt;Treat ERP integration as part of the domain architecture, not as a final connector project.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Transportation Management Solutions are software systems that coordinate shipment planning, execution, tracking, carrier interactions, and transportation analytics. In enterprise environments, they commonly integrate with ERP, warehouse, order management, carrier, and inventory systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should a Transportation Management Solutions use REST APIs or event-driven integration?
&lt;/h3&gt;

&lt;p&gt;Most enterprise transportation platforms benefit from using both. REST APIs are appropriate when an immediate response is required, while event-driven messaging works well for shipment updates, tracking events, notifications, and asynchronous ERP processing.&lt;/p&gt;

&lt;h3&gt;
  
  
  How should duplicate shipment events be handled?
&lt;/h3&gt;

&lt;p&gt;Duplicate events should be handled through idempotent consumers. Each external event should have a unique identifier, which is stored after successful processing. If the same identifier arrives again, the consumer can safely ignore it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why normalize carrier status codes?
&lt;/h3&gt;

&lt;p&gt;Carrier normalization prevents vendor-specific status values from spreading through the application. An integration adapter can convert different external values into a canonical domain state, allowing internal services to process transportation events consistently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can Transportation Management Solutions work with existing ERP systems?
&lt;/h3&gt;

&lt;p&gt;Yes. Transportation Management Solutions can integrate with existing ERP platforms through APIs, middleware, message queues, database interfaces, or scheduled synchronization. The preferred method depends on ERP capabilities, transaction requirements, data ownership, and integration latency.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>logistics</category>
      <category>manufacture</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
