<?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: Suganth</title>
    <description>The latest articles on DEV Community by Suganth (@suganth_g).</description>
    <link>https://dev.to/suganth_g</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%2F3913496%2F64eca758-823b-4c2d-8d10-e7957f9bba09.jpg</url>
      <title>DEV Community: Suganth</title>
      <link>https://dev.to/suganth_g</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/suganth_g"/>
    <language>en</language>
    <item>
      <title>How We Saved Money and Prevented "Ghost" Messages by Respecting Telecom Quiet Hours</title>
      <dc:creator>Suganth</dc:creator>
      <pubDate>Thu, 04 Jun 2026 13:30:11 +0000</pubDate>
      <link>https://dev.to/suganth_g/how-we-saved-money-and-prevented-ghost-messages-by-respecting-telecom-quiet-hours-3m9p</link>
      <guid>https://dev.to/suganth_g/how-we-saved-money-and-prevented-ghost-messages-by-respecting-telecom-quiet-hours-3m9p</guid>
      <description>&lt;p&gt;Building an automated marketing platform comes with technical challenges. &lt;/p&gt;

&lt;p&gt;But sometimes, the biggest challenges aren't technical. They are regulatory.&lt;/p&gt;

&lt;p&gt;At Casa Retail AI, we built a robust journey module that targets specific customer cohorts and executes actions, like sending SMS or RCS messages, automatically.&lt;/p&gt;

&lt;p&gt;But in India, you can't just text customers whenever you want.&lt;/p&gt;

&lt;p&gt;Here is how we implemented a "Quiet Hours" deferral system to comply with telecom laws, save costs, and ensure zero dropped messages.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Context: India's TRAI Regulations
&lt;/h2&gt;

&lt;p&gt;India's Telecom Regulatory Authority of India (TRAI) enforces strict guidelines for commercial communications. &lt;/p&gt;

&lt;p&gt;One of the most important rules is the &lt;strong&gt;"Quiet Hours" restriction&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To protect consumers from spam and disturbances, TRAI mandates that &lt;strong&gt;promotional&lt;/strong&gt; SMS and RCS messages cannot be sent between &lt;strong&gt;9:00 PM and 9:00 AM&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is an important distinction. TRAI quiet hours apply &lt;strong&gt;only to promotional messages&lt;/strong&gt; — re-engagement offers, discount campaigns, win-back journeys, and the like. Transactional messages, such as OTPs, order confirmations, and service alerts, are exempt and can be delivered 24/7.&lt;/p&gt;

&lt;p&gt;Our journey module at Casa is purpose-built for marketing automations — targeting customer cohorts and triggering re-engagement actions. This puts every SMS and RCS event we fire squarely in the &lt;strong&gt;promotional&lt;/strong&gt; category.&lt;/p&gt;

&lt;p&gt;If one of these journeys triggers at 11:30 PM, the telecom provider will intercept and drop the message.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Failed Messages and Hidden Costs
&lt;/h2&gt;

&lt;p&gt;When a promotional journey triggers during quiet hours, the message gets blocked at the telecom provider level.&lt;/p&gt;

&lt;p&gt;But this surfaced two serious problems for our tenants (retailers):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Cost Problem:&lt;/strong&gt; Some third-party message providers charge the tenant for the API request &lt;em&gt;attempt&lt;/em&gt;, even when the message is rejected due to TRAI restrictions. The retailer was being billed for a message their customer never received.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "Ghosting" Problem:&lt;/strong&gt; The retailer intended to send a promotional offer — a re-engagement discount or a win-back campaign. Because the message failed silently at the provider level, the customer never received the offer, and that marketing touchpoint was permanently lost.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is unique to promotional use cases. A transactional OTP or a shipping alert can wait for nothing — it needs to arrive immediately. But a promotional campaign triggered at 11 PM has a natural next-day intent: reach the customer when they are awake.&lt;/p&gt;

&lt;p&gt;We needed a system that intercepted these promotional messages &lt;em&gt;before&lt;/em&gt; they reached the provider, held them safely overnight, and sent them when it was legal to do so.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: The Deferral Queue
&lt;/h2&gt;

&lt;p&gt;We introduced a strict time-window check right before message dispatch.&lt;/p&gt;

&lt;p&gt;If a message attempts to fire outside of the 9 AM - 9 PM window, we halt the dispatch.&lt;/p&gt;

&lt;p&gt;Instead of dropping the message or paying for a failed API call, we push the payload into a separate Kafka log to &lt;code&gt;deferred-sms&lt;/code&gt; | &lt;code&gt;deferred-rcs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The next morning at exactly 9:00 AM, a scheduled worker sweeps these deferred queues and posts the messages back into the primary processing queue.&lt;/p&gt;

&lt;p&gt;To give our tenants flexibility, we made this deferral behavior entirely configurable. Tenants can opt-in to deferral or choose to simply drop the messages if time-sensitivity is critical.&lt;/p&gt;

&lt;p&gt;Finally, every deferred action is logged into ClickHouse. This provides our analytics engine full visibility into how many messages were deferred, allowing tenants to see exactly how their campaigns behave outside of business hours.&lt;/p&gt;




&lt;h2&gt;
  
  
  How it works:
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Timezone Awareness&lt;/strong&gt;: We strictly enforce the &lt;code&gt;Asia/Kolkata&lt;/code&gt; time zone, meaning our platform can be hosted anywhere globally but will always respect Indian local time rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis Caching&lt;/strong&gt;: Configuration reads are cached in Redis for one hour. Since journey engines process thousands of messages per second, hitting PostgreSQL for every message was out of the question.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The Business Impact
&lt;/h2&gt;

&lt;p&gt;This small compliance feature transformed how we pitch our platform's reliability.&lt;/p&gt;

&lt;p&gt;We can now promise our tenants two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No "Ghost" Messages&lt;/strong&gt;: If a customer qualifies for a campaign at midnight, they &lt;em&gt;will&lt;/em&gt; receive the message at 9 AM the next morning. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Wasted Costs&lt;/strong&gt;: Tenants no longer pay external API providers for messages that were guaranteed to fail at the telecom level.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The integration with ClickHouse means that tenants can visualize exactly how much money the deferral queue saved them, building trust in our platform's intelligence.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;Regulatory constraints are not edge cases to route around — they are business rules to design for.&lt;/p&gt;

&lt;p&gt;The natural instinct is to handle a compliance failure at the point of failure: catch the error, log it, move on. But that approach treats the regulation as an exception rather than a known, predictable constraint.&lt;/p&gt;

&lt;p&gt;When we modeled TRAI quiet hours as a first-class concern — one with its own deferral pipeline, its own configuration surface, and its own audit trail in ClickHouse — the system became genuinely more reliable for our tenants.&lt;/p&gt;

&lt;p&gt;Compliance and product quality are not in tension. Handled correctly, they reinforce each other.&lt;/p&gt;

</description>
      <category>scala</category>
      <category>kafka</category>
      <category>telecom</category>
      <category>backend</category>
    </item>
    <item>
      <title>How We Increased Campaign Speed by 10x: Overcoming Third-Party API Bottlenecks</title>
      <dc:creator>Suganth</dc:creator>
      <pubDate>Wed, 03 Jun 2026 10:50:24 +0000</pubDate>
      <link>https://dev.to/suganth_g/how-we-increased-campaign-speed-by-10x-overcoming-third-party-api-bottlenecks-ln6</link>
      <guid>https://dev.to/suganth_g/how-we-increased-campaign-speed-by-10x-overcoming-third-party-api-bottlenecks-ln6</guid>
      <description>&lt;p&gt;When you build a multi-tenant SaaS product, integrating with third-party providers is inevitable.&lt;/p&gt;

&lt;p&gt;But what happens when their latency becomes your bottleneck?&lt;/p&gt;

&lt;p&gt;Recently, we encountered a massive performance issue in our notification engine that was stalling campaigns for hours. &lt;/p&gt;

&lt;p&gt;Here is how we solved it and increased our campaign dispatch speed by 10x.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Context: Journey-Based WhatsApp Campaigns
&lt;/h1&gt;

&lt;p&gt;At Casa Retail AI, we have a journey flow where retailers can target specific groups of customers. &lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Customers with a Lifetime Value (LTV) greater than ₹10,000.&lt;/li&gt;
&lt;li&gt;Customers who haven’t made a purchase in the past year.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once these cohorts are identified, the system sends them a targeted WhatsApp message to re-engage them.&lt;/p&gt;

&lt;p&gt;Because we operate a multi-tenant platform, every tenant (retailer) has their own distinct WhatsApp service provider integrated with Casa. &lt;/p&gt;

&lt;p&gt;The workflow seemed straightforward: identify the audience, construct the payloads, and push them to the provider.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Problem: The API Latency Bottleneck
&lt;/h1&gt;

&lt;p&gt;The issue wasn’t our database. &lt;/p&gt;

&lt;p&gt;It wasn't our background job queue either. &lt;/p&gt;

&lt;p&gt;The bottleneck was the WhatsApp message providers.&lt;/p&gt;

&lt;p&gt;While sending messages, we discovered that some third-party providers took up to 2 seconds to process a single request. &lt;/p&gt;

&lt;p&gt;If a tenant launched a campaign targeting a large cohort, processing these messages sequentially was agonizingly slow. &lt;/p&gt;

&lt;p&gt;A single campaign could take approximately 5 hours to complete.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Real Issue: The "Innocent Bystander" Problem
&lt;/h1&gt;

&lt;p&gt;A 5-hour campaign doesn't just affect the tenant running it. &lt;/p&gt;

&lt;p&gt;Because we use a shared background job system, those slow jobs were hogging the workers. &lt;/p&gt;

&lt;p&gt;Other tenants' jobs were sitting in the queue, waiting for execution. &lt;/p&gt;

&lt;p&gt;It wasn’t the other tenants' fault. &lt;/p&gt;

&lt;p&gt;It wasn't even the active tenant's fault—they just had a slow provider.&lt;/p&gt;

&lt;p&gt;We initially tried to mitigate this by routing payloads to a &lt;code&gt;delayed-queue&lt;/code&gt; based on the average response time for that particular campaign. &lt;/p&gt;

&lt;p&gt;While this prevented the main queue from grinding to a halt, it was merely treating the symptom, not the root cause. &lt;/p&gt;

&lt;p&gt;We needed a way to dispatch these requests much faster.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Solution: The Concurrent Dispatcher
&lt;/h1&gt;

&lt;p&gt;If a single API call takes 2 seconds, calling it 1,000 times in sequence takes 2,000 seconds. &lt;/p&gt;

&lt;p&gt;But if we can run them in parallel, we only pay the latency cost of the slowest concurrent batch.&lt;/p&gt;

&lt;p&gt;To achieve this, we built a &lt;strong&gt;Concurrent Dispatcher&lt;/strong&gt; in Scala to process these external API calls simultaneously.&lt;/p&gt;

&lt;p&gt;The idea was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Instead of sending one message at a time, group the tasks and fire them concurrently using a dedicated thread pool, awaiting their completion before proceeding.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How It Works:
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dedicated Thread Pool&lt;/strong&gt;: We use a &lt;code&gt;FixedThreadPool&lt;/code&gt; with a configurable concurrency limit (defaulting to 30). This isolates these slow, I/O-bound API tasks from the rest of the application's execution context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Future Wrapping&lt;/strong&gt;: Each task is wrapped in a Scala &lt;code&gt;Future&lt;/code&gt;, allowing them to execute immediately on the dedicated thread pool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Future.sequence&lt;/strong&gt;: This powerful Scala standard library method transforms a &lt;code&gt;Seq[Future[T]]&lt;/code&gt; into a &lt;code&gt;Future[Seq[T]]&lt;/code&gt;, effectively combining them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bounded Awaiting&lt;/strong&gt;: We &lt;code&gt;Await.result&lt;/code&gt; with a configurable timeout (defaulting to 5 minutes) to ensure a hung third-party API doesn't hold our workers hostage forever.&lt;/li&gt;
&lt;/ol&gt;




&lt;h1&gt;
  
  
  The Business Impact
&lt;/h1&gt;

&lt;p&gt;The results were immediate and drastic.&lt;/p&gt;

&lt;p&gt;By dispatching requests concurrently, a campaign that previously took &lt;strong&gt;5 hours&lt;/strong&gt; now finished in roughly a fraction of the time.&lt;/p&gt;

&lt;p&gt;That's a &lt;strong&gt;massive 10x improvement in campaign speed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Because campaigns finished faster:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Workers were freed up sooner.&lt;/li&gt;
&lt;li&gt;Other tenants no longer experienced phantom delays in the job queue.&lt;/li&gt;
&lt;li&gt;We no longer had to aggressively route tasks to a delayed queue just to survive high traffic.&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  What I Learned
&lt;/h1&gt;

&lt;p&gt;When you integrate with external systems, you are bound by their worst-case latency. &lt;/p&gt;

&lt;p&gt;If you process external I/O sequentially in a background worker, you are turning a fast, internal asynchronous system into a slow, synchronous one.&lt;/p&gt;

&lt;p&gt;Sometimes, the best solution is simply refusing to wait in line. &lt;/p&gt;

&lt;p&gt;By recognizing that our bottleneck was external I/O wait time, introducing a concurrent execution model allowed us to reclaim our system's throughput.&lt;/p&gt;

</description>
      <category>scala</category>
      <category>kafka</category>
      <category>backend</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Ticket Desk: A Multi-Tenant CRM Helpdesk at the Heart of Retail</title>
      <dc:creator>Suganth</dc:creator>
      <pubDate>Mon, 01 Jun 2026 10:27:40 +0000</pubDate>
      <link>https://dev.to/suganth_g/ticket-desk-a-multi-tenant-crm-helpdesk-at-the-heart-of-retail-1lp5</link>
      <guid>https://dev.to/suganth_g/ticket-desk-a-multi-tenant-crm-helpdesk-at-the-heart-of-retail-1lp5</guid>
      <description>&lt;p&gt;When people picture a ticketing system, they usually imagine a basic support inbox — customer sends a message, agent replies, ticket closed.&lt;/p&gt;

&lt;p&gt;What we built at Casa Retail AI is something very different.&lt;/p&gt;

&lt;p&gt;Ticket Desk is not just a helpdesk. It is the customer issue resolution layer of a full-stack retail CRM and CDP platform, tightly integrated with products, stores, digital receipts, journeys, leads, and analytics. Every ticket is a thread connected to the customer's purchase history, their store, their product, and the SLAs a brand has committed to.&lt;/p&gt;

&lt;p&gt;This is the story of how we built it — and what made it genuinely complex.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Context: What is Casa Retail AI?
&lt;/h2&gt;

&lt;p&gt;At &lt;a href="https://casaretail.ai/" rel="noopener noreferrer"&gt;Casa Retail AI&lt;/a&gt;, we build a multi-vendor, multi-tenant CRM and Customer Data Platform designed for the Indian retail ecosystem.&lt;/p&gt;

&lt;p&gt;A single deployment of Casa serves multiple retail brands — each brand is a tenant. Each tenant may have dozens of stores, hundreds of agents, thousands of customers, and millions of interactions across channels.&lt;/p&gt;

&lt;p&gt;Our platform handles marketing automation, loyalty, product catalogs, digital receipts, lead management, and customer support — all under one roof. Every module is interconnected.&lt;/p&gt;

&lt;p&gt;Ticket Desk is the customer support module. But the word "support" undersells it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Making It Multi-Tenant
&lt;/h2&gt;

&lt;p&gt;The first and most foundational challenge was retrofitting multi-tenancy into a system that wasn't designed for it.&lt;/p&gt;

&lt;p&gt;Every record — tickets, agents, groups, users, SLA policies, triggers — had to be scoped to a tenant. Every query, every API response, every background job needed to be tenant-aware.&lt;/p&gt;

&lt;p&gt;Beyond isolation, each tenant needed their own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agent and group hierarchy&lt;/li&gt;
&lt;li&gt;Role and permission configuration&lt;/li&gt;
&lt;li&gt;SLA policies and business hours&lt;/li&gt;
&lt;li&gt;Custom ticket fields&lt;/li&gt;
&lt;li&gt;Channel configurations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And because Ticket Desk sits inside the Casa ecosystem, every tenant's data had to stay in sync with their stores, their product catalog, and their customer base — all of which live in other modules.&lt;/p&gt;

&lt;p&gt;This tenant boundary is not just a database concern. It shapes every layer of the system: routing, authorization, background jobs, analytics pipelines, and API design.&lt;/p&gt;




&lt;h2&gt;
  
  
  Multi-Source Ticket Ingestion
&lt;/h2&gt;

&lt;p&gt;Once multi-tenancy was in place, the next challenge was how tickets enter the system.&lt;/p&gt;

&lt;p&gt;Customers don't raise issues through a single channel. A retail brand receives complaints and requests from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Direct&lt;/strong&gt; — agents creating tickets on behalf of walk-in customers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email&lt;/strong&gt; — customer emails routed into the system as tickets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WhatsApp&lt;/strong&gt; — messages received via WhatsApp Business API, converted into ticket threads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Digital Receipt&lt;/strong&gt; — a customer raising a ticket directly from the e-bill they received after purchase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each channel has a different payload shape, different metadata, and different expectations for how the ticket should be created.&lt;/p&gt;

&lt;p&gt;But once a ticket is created — regardless of source — it flows through the same core pipeline. Same SLA tracking, same agent assignment logic, same trigger-based workflows.&lt;/p&gt;

&lt;p&gt;The channel is context. The ticket is the unit of work.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Digital Receipt Flow
&lt;/h2&gt;

&lt;p&gt;The digital receipt integration deserves its own mention because of how seamlessly it closes the loop between purchase and support.&lt;/p&gt;

&lt;p&gt;When a customer makes a purchase at a retail store, they receive a digital receipt — delivered via WhatsApp, SMS, or email. That receipt is not just a bill. It is an entry point back into the platform.&lt;/p&gt;

&lt;p&gt;Embedded in that receipt is a way for the customer to raise a ticket. They can report a damaged product, request a replacement, or flag a service issue — all without knowing they're interacting with a helpdesk system.&lt;/p&gt;

&lt;p&gt;When they do, the ticket is created automatically, pre-populated with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The store where the purchase happened&lt;/li&gt;
&lt;li&gt;The product they bought&lt;/li&gt;
&lt;li&gt;The invoice details&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent receiving that ticket has full context before they say a word.&lt;/p&gt;




&lt;h2&gt;
  
  
  Product and Store Mapping
&lt;/h2&gt;

&lt;p&gt;One of the biggest extensions we built on top of Zammad's core was the ability to link tickets to specific products and stores.&lt;/p&gt;

&lt;p&gt;In a retail context, this matters enormously.&lt;/p&gt;

&lt;p&gt;When a customer raises a complaint about a refrigerator they bought, the agent needs to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which product (brand, model, SKU)&lt;/li&gt;
&lt;li&gt;Which store it was purchased from&lt;/li&gt;
&lt;li&gt;Which store is responsible for resolution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We built a product-linking widget inside the ticket view that connects directly to Commerce Connect — our internal Product Information Management system. Agents can search and attach the relevant product to a ticket in seconds.&lt;/p&gt;

&lt;p&gt;Store-level grouping works alongside this. Tickets can be routed to the group responsible for a specific store, ensuring the right team handles the issue.&lt;/p&gt;

&lt;p&gt;This transforms the ticket from a generic support record into a rich operational object tied to real retail data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Journey-Driven Ticket Creation
&lt;/h2&gt;

&lt;p&gt;Not all tickets are created by humans.&lt;/p&gt;

&lt;p&gt;Some of our most valuable tickets are created automatically by our Journey module — a trigger-based automation engine that can initiate actions based on customer data and time conditions.&lt;/p&gt;

&lt;p&gt;The clearest example comes from consumer electronics and appliance retailers like large electronics chains.&lt;/p&gt;

&lt;p&gt;When a customer buys an appliance, their purchase is recorded. The journey engine monitors the elapsed time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;6 months after purchase&lt;/strong&gt; → a service ticket is auto-created, prompting a routine check-in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 year after purchase&lt;/strong&gt; → a follow-up service ticket is created, aligned with warranty terms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent receives a fully formed ticket with all the context: who the customer is, what they bought, when they bought it, and what the service expectation is.&lt;/p&gt;

&lt;p&gt;This is not reactive support. It is proactive, scheduled, warranty-aware customer service — driven by business rules configured once and executed at scale.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ticket-to-Lead Conversion
&lt;/h2&gt;

&lt;p&gt;Sometimes a support issue reveals a sales opportunity.&lt;/p&gt;

&lt;p&gt;Consider a customer who calls in because the display on their phone is cracked. The agent checks with the service team and finds that the spare part is not available. The product cannot be repaired.&lt;/p&gt;

&lt;p&gt;In most systems, that ticket would be closed as unresolved.&lt;/p&gt;

&lt;p&gt;In Ticket Desk, the agent can convert that ticket into a lead — flagging the customer as a potential buyer for a replacement or upgrade. The lead flows directly into our Lead Management module, where a sales agent can follow up with the right product recommendation.&lt;/p&gt;

&lt;p&gt;This is a business workflow that only makes sense inside a unified CRM platform. Ticket Desk is connected to the lead pipeline because they share the same customer record, the same product catalog, and the same organizational hierarchy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Beyond Customer Support: Internal and Operational Tickets
&lt;/h2&gt;

&lt;p&gt;Not every ticket in Ticket Desk originates from a customer complaint.&lt;/p&gt;

&lt;p&gt;One of the less obvious — but genuinely useful — uses of the system is as an internal coordination and operations layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Journey Opportunity Suggestions
&lt;/h3&gt;

&lt;p&gt;The Journey module identifies segments of customers who qualify for a targeted campaign — a loyalty offer, a win-back promotion, a seasonal push. But acting on those insights requires a human to sign off, configure, and launch.&lt;/p&gt;

&lt;p&gt;Instead of that decision sitting in a dashboard nobody checks, Ticket Desk creates an internal ticket assigned to the relevant marketing or CRM team.&lt;/p&gt;

&lt;p&gt;The ticket surfaces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The customer segment identified&lt;/li&gt;
&lt;li&gt;The opportunity type (reactivation, upsell, event-based)&lt;/li&gt;
&lt;li&gt;The suggested journey to run&lt;/li&gt;
&lt;li&gt;Context on why this segment was flagged&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The team reviews it, approves or modifies the journey, and closes the ticket once executed. It keeps the insight actionable and the process auditable — every opportunity suggestion has a paper trail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Operational Tickets
&lt;/h3&gt;

&lt;p&gt;Beyond journey suggestions, Ticket Desk also handles general operational work that doesn't fit neatly into a customer support flow — store onboarding tasks, internal escalations, vendor coordination items, and process exceptions that need assignment and tracking.&lt;/p&gt;

&lt;p&gt;Using the same ticket infrastructure for internal operations meant we didn't have to build or integrate a separate project management or task tracking tool. The same SLA rules, group routing, trigger workflows, and reporting pipeline that handles customer tickets handles operational ones too.&lt;/p&gt;

&lt;p&gt;It is a small thing architecturally, but it significantly increased how much the platform was used day-to-day across teams.&lt;/p&gt;




&lt;h2&gt;
  
  
  Analytics: What the Tickets Tell Us
&lt;/h2&gt;

&lt;p&gt;After a ticket is resolved, the work isn't done.&lt;/p&gt;

&lt;p&gt;All ticket data — creation, updates, resolution times, linked products, linked stores, assigned groups, and SLA outcomes — is synchronized to ClickHouse. This is the same ClickHouse cluster that powers analytics across the rest of the Casa platform.&lt;/p&gt;

&lt;p&gt;On top of this data, we built dashboards that let brand and operations teams answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which stores are generating the most complaints?&lt;/li&gt;
&lt;li&gt;Which products have the highest issue frequency?&lt;/li&gt;
&lt;li&gt;Which categories have the worst resolution times?&lt;/li&gt;
&lt;li&gt;How are agents performing against SLA targets?&lt;/li&gt;
&lt;li&gt;Are certain store clusters systematically underperforming?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not just a support dashboard. It is a retail operations intelligence tool.&lt;/p&gt;

&lt;p&gt;The connection between tickets and products means that a pattern of complaints about a specific SKU shows up in the data before it becomes a brand reputation problem. Store managers can act on it. Buyers can flag it with the vendor. The loop closes.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Made This Hard
&lt;/h2&gt;

&lt;p&gt;Looking back, the technical challenges weren't in any single feature. They were in the integration surface.&lt;/p&gt;

&lt;p&gt;Ticket Desk talks to nearly every other Casa module:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Commerce Connect&lt;/strong&gt; for product data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store Management&lt;/strong&gt; for store and agent hierarchy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Digital Receipts&lt;/strong&gt; for purchase context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Journey Engine&lt;/strong&gt; for automated ticket creation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lead Management&lt;/strong&gt; for ticket-to-lead conversion&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ClickHouse&lt;/strong&gt; for analytics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping all of these integrations consistent — as each module evolved independently — required careful API design, reliable event handling, and a shared understanding of the tenant boundary across teams.&lt;/p&gt;

&lt;p&gt;The other challenge was the Zammad fork itself. Extending an open-source platform that wasn't designed for your requirements means you own every deviation. Every upstream change becomes a merge decision. Every extension has to live alongside the original code without breaking what already worked.&lt;/p&gt;

&lt;p&gt;That discipline — knowing what to extend, what to replace, and what to leave alone — was as important as any technical decision we made.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;Building Ticket Desk reinforced something I believe more firmly with every product I ship:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The value of a platform is not in its individual features. It is in how those features connect.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A digital receipt by itself is just a bill. A ticket by itself is just a support record. A product record by itself is just a data entry.&lt;/p&gt;

&lt;p&gt;But when a customer raises a complaint from their receipt, the ticket arrives pre-loaded with their product and store context, gets assigned to the right team by a trigger, tracked against an SLA, and — if the product can't be repaired — flows into the lead pipeline for a sales follow-up…&lt;/p&gt;

&lt;p&gt;That is not just engineering. That is a complete customer experience, built out of many small, well-connected systems.&lt;/p&gt;

&lt;p&gt;That is what we built with Ticket Desk.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjt89dmn72k94lzsso6fh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjt89dmn72k94lzsso6fh.png" alt=" " width="800" height="306"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Ticket Desk is part of the &lt;a href="https://casaretail.ai/" rel="noopener noreferrer"&gt;Casa Retail AI&lt;/a&gt; platform — a multi-tenant CRM and CDP for the Indian retail ecosystem.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>crm</category>
      <category>rails</category>
      <category>ticket</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>How We Stopped Tenants From Hogging the Job Queue - Tenant-Level Parallel Job Limiting in Sidekiq</title>
      <dc:creator>Suganth</dc:creator>
      <pubDate>Mon, 01 Jun 2026 08:25:02 +0000</pubDate>
      <link>https://dev.to/suganth_g/how-we-stopped-tenants-from-hogging-the-job-queue-tenant-level-parallel-job-limiting-in-sidekiq-2aeg</link>
      <guid>https://dev.to/suganth_g/how-we-stopped-tenants-from-hogging-the-job-queue-tenant-level-parallel-job-limiting-in-sidekiq-2aeg</guid>
      <description>&lt;h2&gt;
  
  
  The Context: What is Commerce Connect?
&lt;/h2&gt;

&lt;p&gt;At Casa Retail AI, we have an internal platform called &lt;strong&gt;Commerce Connect (CC)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Commerce Connect acts as the central &lt;strong&gt;Product Information Management (PIM)&lt;/strong&gt; system — the single source of truth for product data across our entire retail ecosystem. Built on top of a customized version of the open-source e-commerce platform Spree Commerce, it is extended with multi-vendor and multi-tenant capabilities.&lt;/p&gt;

&lt;p&gt;Its job is straightforward:&lt;br&gt;
Collect product information from multiple retail ecosystems and distribute it to every Casa product that needs it — PostgreSQL for operational modules, ClickHouse for analytics, and external B2B integrations for partner systems.&lt;/p&gt;

&lt;p&gt;All of this synchronization work happens in the background using &lt;strong&gt;Sidekiq&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: A Shared Queue With No Per-Tenant Guardrails
&lt;/h2&gt;

&lt;p&gt;Sidekiq processes jobs concurrently using a fixed pool of worker threads. At any given time, there is a hard cap on how many jobs can run simultaneously across the entire system.&lt;/p&gt;

&lt;p&gt;In practice, this meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A tenant uploading multiple CSV files in quick succession&lt;/li&gt;
&lt;li&gt;A tenant triggering multiple sync operations to different external systems&lt;/li&gt;
&lt;li&gt;Multiple tenants doing all of this at the same time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There was nothing stopping a single active tenant from claiming the majority of worker slots. If one tenant fired several heavy jobs back-to-back, every other tenant's jobs sat in the queue waiting — with no visibility into why.&lt;/p&gt;

&lt;p&gt;This wasn't theoretical. As we onboarded more tenants, the pattern became real:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Smaller tenants experienced noticeable delays during high-activity periods from larger ones.&lt;/li&gt;
&lt;li&gt;Large catalog syncs from one tenant could hold up all other background work.&lt;/li&gt;
&lt;li&gt;There was no way to offer differentiated service — every tenant was treated identically, regardless of their plan or size.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We needed a way to &lt;strong&gt;restrict how many jobs a single tenant could run in parallel&lt;/strong&gt;, at the queue level.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: Per-Tenant Job Slot Tracking
&lt;/h2&gt;

&lt;p&gt;The idea was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Each tenant gets a configurable maximum number of concurrent jobs per queue. If a job tries to run and the tenant is already at their limit, reschedule it for later and let the next tenant go.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We introduced a database record per tenant per queue, tracking two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;max_job&lt;/code&gt;&lt;/strong&gt; — how many parallel jobs this tenant is allowed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;current_job&lt;/code&gt;&lt;/strong&gt; — how many are currently running&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before any background job executes, it must &lt;strong&gt;acquire a slot&lt;/strong&gt;. When it finishes — or fails — it &lt;strong&gt;releases the slot&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For most tenants, the default is &lt;code&gt;max_job: 1&lt;/code&gt;. Single-job concurrency. Simple, predictable, fair.&lt;br&gt;
For premium tenants with higher throughput requirements, we raise the limit — no code change required, just a configuration update.&lt;/p&gt;

&lt;p&gt;The limit is also &lt;strong&gt;queue-aware&lt;/strong&gt;. A tenant's slot count on the CSV import queue is tracked independently from the sync queue. One slow upload doesn't block a faster downstream sync for the same tenant.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Flow
&lt;/h2&gt;

&lt;p&gt;When a job is picked up by a Sidekiq worker, before any real business logic runs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Look up the tenant's config record for this queue. Create it with defaults if it doesn't exist yet — new tenants need no manual setup.&lt;/li&gt;
&lt;li&gt;Check if a slot is available (&lt;code&gt;current_job &amp;lt; max_job&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;If not — reschedule the job for 1 minute later and return. The work still gets done; it just waits its turn.&lt;/li&gt;
&lt;li&gt;If yes — increment &lt;code&gt;current_job&lt;/code&gt; and proceed with the actual job.&lt;/li&gt;
&lt;li&gt;When the job finishes (or raises an exception), decrement &lt;code&gt;current_job&lt;/code&gt; back.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The decrement in step 5 always happens — even on failures. This is the guarantee that prevents a crashed job from permanently consuming a slot and slowly starving the tenant out of all future work.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Race Condition — and How We Handle It
&lt;/h2&gt;

&lt;p&gt;This is where it gets interesting.&lt;/p&gt;

&lt;p&gt;At first glance, "check if slots are available, then increment" sounds simple. But in a concurrent system with multiple Sidekiq threads running simultaneously, this is a classic &lt;strong&gt;read-modify-write race condition&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Imagine two jobs for the same tenant land in the queue at almost the same time. Both threads check &lt;code&gt;current_job &amp;lt; max_job&lt;/code&gt;. Both see the count is within limits. Both decide to proceed. Both increment. Now the tenant is running more concurrent jobs than allowed — the limit is silently violated.&lt;/p&gt;

&lt;p&gt;The naive fix — read the value, check it in application code, then write it back — doesn't hold up under concurrency. There's always a window between the read and the write where another thread can slip through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The correct fix is to make the check and the increment a single atomic operation at the database level.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of reading the value and then updating it separately, we issue a conditional SQL update that does both in one statement: "increment &lt;code&gt;current_job&lt;/code&gt; by 1, but only if &lt;code&gt;current_job&lt;/code&gt; is currently less than &lt;code&gt;max_job&lt;/code&gt;." The database processes this as one indivisible operation. If two threads race to the same record simultaneously, only one of them will satisfy the condition — the other will see that zero rows were updated, and knows it lost the race.&lt;/p&gt;

&lt;p&gt;No Redis locks. No application-level mutexes. No external coordination. The database's own row-level locking gives us the guarantee we need, for free.&lt;/p&gt;

&lt;p&gt;The same pattern applies on release: the decrement is conditioned on &lt;code&gt;current_job &amp;gt; 0&lt;/code&gt;, preventing the counter from going negative if release is somehow called more times than acquire in an unexpected error flow.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Business Impact
&lt;/h2&gt;

&lt;p&gt;This was a small module by any measure. But the effect on fairness across tenants was immediate.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No single tenant can monopolize the job queue, regardless of how aggressively they upload or sync.&lt;/li&gt;
&lt;li&gt;Most tenants run comfortably on the default single-slot limit — simple, first-in-first-out behavior.&lt;/li&gt;
&lt;li&gt;Premium tenants with higher throughput needs get their limit raised with a simple config update.&lt;/li&gt;
&lt;li&gt;No new infrastructure. No Redis, no distributed lock service — the existing database was enough.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;The most effective solutions are often the ones that fit the shape of the system you already have.&lt;/p&gt;

&lt;p&gt;We had PostgreSQL. We had Sidekiq. We didn't need a new component.&lt;/p&gt;

&lt;p&gt;One database table. One atomic update pattern. One shared module any worker can include.&lt;/p&gt;

&lt;p&gt;That was enough to give every tenant a fair share of the job queue — regardless of how active their neighbors were.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: &lt;code&gt;#rails&lt;/code&gt; &lt;code&gt;#sidekiq&lt;/code&gt; &lt;code&gt;#ruby&lt;/code&gt; &lt;code&gt;#backgroundjobs&lt;/code&gt; &lt;code&gt;#softwareengineering&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rails</category>
      <category>backend</category>
      <category>ruby</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>How a Small Product Sync Automation Changed Onboarding at Scale</title>
      <dc:creator>Suganth</dc:creator>
      <pubDate>Sun, 31 May 2026 15:35:34 +0000</pubDate>
      <link>https://dev.to/suganth_g/how-a-small-product-sync-automation-changed-onboarding-at-scale-57de</link>
      <guid>https://dev.to/suganth_g/how-a-small-product-sync-automation-changed-onboarding-at-scale-57de</guid>
      <description>&lt;h1&gt;
  
  
  How a Product Sync Automation Project Transformed Customer Onboarding
&lt;/h1&gt;

&lt;p&gt;When people think about impactful engineering work, they often imagine distributed systems, high-scale infrastructure, or complex algorithms.&lt;/p&gt;

&lt;p&gt;One of the most impactful projects I worked on wasn't any of those.&lt;/p&gt;

&lt;p&gt;It was solving a seemingly simple problem:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keeping product data in sync across multiple retail systems.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Years later, our CEO still remembers how much smoother customer onboarding became after this project.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Context: What is Commerce Connect?
&lt;/h1&gt;

&lt;p&gt;At Casa Retail AI, we have an internal platform called &lt;strong&gt;Commerce Connect (CC)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Commerce Connect acts as the central &lt;strong&gt;Product Information Management (PIM)&lt;/strong&gt; system and serves as the &lt;strong&gt;source of truth&lt;/strong&gt; for product information.&lt;/p&gt;

&lt;p&gt;Under the hood, it is built on top of a customized version of the open-source e-commerce platform &lt;strong&gt;Spree Commerce&lt;/strong&gt;, extended with multi-vendor and multi-tenant capabilities.&lt;/p&gt;

&lt;p&gt;Its primary responsibility is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Collect product information from multiple retail ecosystems and distribute it to every Casa product that needs it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once product data enters Commerce Connect, it is synchronized to multiple downstream systems.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Product Data Matters
&lt;/h1&gt;

&lt;p&gt;Many applications inside Casa depend on product information.&lt;/p&gt;

&lt;h2&gt;
  
  
  Product Consumers
&lt;/h2&gt;

&lt;p&gt;Once product data enters Commerce Connect, it is distributed to multiple systems across the Casa ecosystem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Customer-Facing Applications
&lt;/h3&gt;

&lt;p&gt;Several products rely on product information to provide context and improve customer experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lead management applications use product information during customer interactions.&lt;/li&gt;
&lt;li&gt;Ticket management systems link customer issues to specific products.&lt;/li&gt;
&lt;li&gt;Digital receipts display product names, images, and related details.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Analytics &amp;amp; Reporting
&lt;/h3&gt;

&lt;p&gt;Product data powers business dashboards and reports, helping retailers answer questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which categories perform best?&lt;/li&gt;
&lt;li&gt;Which products attract the most attention?&lt;/li&gt;
&lt;li&gt;Which products generate the most complaints?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is also used for filtering and segmentation across various dashboards.&lt;/p&gt;

&lt;h3&gt;
  
  
  External Integrations
&lt;/h3&gt;

&lt;p&gt;Some tenants require product synchronization with external platforms, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;B2B applications&lt;/li&gt;
&lt;li&gt;E-commerce experiences&lt;/li&gt;
&lt;li&gt;Digital catalog/flipbook solutions&lt;/li&gt;
&lt;li&gt;Custom third-party integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because multiple systems depend on the same product catalog, maintaining a single source of truth is critical.&lt;/p&gt;

&lt;p&gt;This ensures every system across the ecosystem works with consistent and up-to-date product information.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Original Process
&lt;/h1&gt;

&lt;p&gt;Whenever a new tenant was onboarded, the first challenge was importing their entire product catalog.&lt;/p&gt;

&lt;p&gt;Most retailers maintained product data in POS systems such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SAP&lt;/li&gt;
&lt;li&gt;Wondersoft&lt;/li&gt;
&lt;li&gt;APX&lt;/li&gt;
&lt;li&gt;Other proprietary retail platforms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The onboarding process looked like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Customer Success (CS) teams collected product exports from the tenant.&lt;/li&gt;
&lt;li&gt;The tenant provided CSV files containing product information.&lt;/li&gt;
&lt;li&gt;CS teams manually mapped CSV columns to Commerce Connect fields.&lt;/li&gt;
&lt;li&gt;The CSV was uploaded into Commerce Connect.&lt;/li&gt;
&lt;li&gt;Each upload created a batch task.&lt;/li&gt;
&lt;li&gt;Background workers processed the batch and created products inside Commerce Connect.&lt;/li&gt;
&lt;li&gt;After products were created, additional synchronization jobs were triggered for:

&lt;ul&gt;
&lt;li&gt;PostgreSQL&lt;/li&gt;
&lt;li&gt;ClickHouse&lt;/li&gt;
&lt;li&gt;B2B applications&lt;/li&gt;
&lt;li&gt;Other enabled integrations&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Visually, the flow looked something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POS Export
    ↓
CSV File
    ↓
Manual Mapping
    ↓
Commerce Connect
    ↓
Batch Processing
    ↓
PG / ClickHouse / B2B Sync
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, this seemed reasonable.&lt;/p&gt;

&lt;p&gt;But as the business scaled, the cracks started to show.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Problems We Started Seeing
&lt;/h1&gt;

&lt;h2&gt;
  
  
  1. Product Data Became a Bottleneck
&lt;/h2&gt;

&lt;p&gt;A tenant could not fully use the platform until their product catalog was available.&lt;/p&gt;

&lt;p&gt;This meant onboarding timelines were heavily dependent on receiving CSV files from the customer.&lt;/p&gt;

&lt;p&gt;Sometimes the customer took days to provide them.&lt;/p&gt;

&lt;p&gt;Sometimes the files were incomplete.&lt;/p&gt;

&lt;p&gt;Sometimes they contained incorrect mappings.&lt;/p&gt;

&lt;p&gt;Engineering wasn't the bottleneck.&lt;/p&gt;

&lt;p&gt;Product data was.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Keeping Products Updated Was Painful
&lt;/h2&gt;

&lt;p&gt;Product catalogs constantly change.&lt;/p&gt;

&lt;p&gt;New products get added.&lt;/p&gt;

&lt;p&gt;Existing products get updated.&lt;/p&gt;

&lt;p&gt;Products become inactive.&lt;/p&gt;

&lt;p&gt;All of these changes happen in the retailer's POS system.&lt;/p&gt;

&lt;p&gt;Whenever a tenant changed something in their POS, Customer Success teams had to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request updated exports.&lt;/li&gt;
&lt;li&gt;Receive new CSV files.&lt;/li&gt;
&lt;li&gt;Upload them again.&lt;/li&gt;
&lt;li&gt;Reprocess the entire catalog.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping thousands of products synchronized through spreadsheets quickly became operationally expensive.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Every Tenant Required Manual Configuration
&lt;/h2&gt;

&lt;p&gt;Different POS providers generated different CSV formats.&lt;/p&gt;

&lt;p&gt;The Customer Success team had to repeatedly configure mappings for every upload.&lt;/p&gt;

&lt;p&gt;The process was repetitive, error-prone, and difficult to scale.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Key Observation
&lt;/h2&gt;

&lt;p&gt;The product data already existed inside the POS systems.&lt;/p&gt;

&lt;p&gt;Why were we asking humans to manually export and upload it?&lt;/p&gt;

&lt;p&gt;Why not fetch it directly from the source?&lt;/p&gt;




&lt;h1&gt;
  
  
  The Solution: Pull Products Directly From POS Providers
&lt;/h1&gt;

&lt;p&gt;SAP was the first provider that offered APIs for product retrieval.&lt;/p&gt;

&lt;p&gt;Instead of waiting for CSV exports, we integrated directly with their APIs and automatically imported products into Commerce Connect.&lt;/p&gt;

&lt;p&gt;The result was immediate.&lt;/p&gt;

&lt;p&gt;No spreadsheets.&lt;/p&gt;

&lt;p&gt;No manual exports.&lt;/p&gt;

&lt;p&gt;No repeated uploads.&lt;/p&gt;

&lt;p&gt;Just product synchronization directly from the source system.&lt;/p&gt;

&lt;p&gt;After the success of the SAP integration, we expanded the same approach to other POS providers.&lt;/p&gt;




&lt;h1&gt;
  
  
  Designing for Scale
&lt;/h1&gt;

&lt;p&gt;One requirement was clear:&lt;/p&gt;

&lt;p&gt;We didn't want to rewrite the entire synchronization pipeline every time a new POS provider was added.&lt;/p&gt;

&lt;p&gt;So we introduced a provider abstraction using a Factory Pattern.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"sap_iplanet"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The factory dynamically resolves and instantiates the appropriate provider implementation.&lt;/p&gt;

&lt;p&gt;Each provider only needs to answer a few questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to authenticate?&lt;/li&gt;
&lt;li&gt;How to fetch products?&lt;/li&gt;
&lt;li&gt;How to transform data?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else remains shared.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POS Provider
      ↓
Provider Adapter
      ↓
Commerce Connect
      ↓
Existing Sync Pipeline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gave us a huge advantage.&lt;/p&gt;

&lt;p&gt;Adding a new POS integration became as simple as creating a new provider class.&lt;/p&gt;

&lt;p&gt;The provider automatically inherited:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Batch processing&lt;/li&gt;
&lt;li&gt;Error handling&lt;/li&gt;
&lt;li&gt;Task creation&lt;/li&gt;
&lt;li&gt;Product synchronization&lt;/li&gt;
&lt;li&gt;PostgreSQL sync&lt;/li&gt;
&lt;li&gt;ClickHouse sync&lt;/li&gt;
&lt;li&gt;B2B integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The integration surface became dramatically smaller.&lt;/p&gt;




&lt;h1&gt;
  
  
  Eliminating Repetitive Mapping Work
&lt;/h1&gt;

&lt;p&gt;The old CSV process required Customer Success teams to repeatedly map columns.&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 plaintext"&gt;&lt;code&gt;CSV Column          → Casa Field

item_code           → SKU
item_name           → Product Name
dept_name           → Category
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every upload carried the risk of mapping mistakes.&lt;/p&gt;

&lt;p&gt;With APIs, we could see the complete product schema exposed by the provider.&lt;/p&gt;

&lt;p&gt;The mapping became a one-time configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Provider Field      → Casa Field

item_code           → SKU
item_name           → Product Name
dept_name           → Category
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After configuration, all future synchronizations happened automatically.&lt;/p&gt;

&lt;p&gt;This significantly reduced operational effort.&lt;/p&gt;




&lt;h1&gt;
  
  
  More Control Over Synchronization
&lt;/h1&gt;

&lt;p&gt;The CSV approach only supported full catalog uploads.&lt;/p&gt;

&lt;p&gt;Whether one product changed or ten thousand products changed, the entire file was processed again.&lt;/p&gt;

&lt;p&gt;With API-based synchronization, we gained much more control.&lt;/p&gt;

&lt;p&gt;We could:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pull products on a schedule.&lt;/li&gt;
&lt;li&gt;Fetch only relevant updates.&lt;/li&gt;
&lt;li&gt;Track synchronization status.&lt;/li&gt;
&lt;li&gt;Improve error visibility.&lt;/li&gt;
&lt;li&gt;Retry failed imports independently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The synchronization process became far more predictable and manageable.&lt;/p&gt;




&lt;h1&gt;
  
  
  Solving the "I Can't Wait Until Tomorrow" Problem
&lt;/h1&gt;

&lt;p&gt;While nightly synchronization worked for most tenants, some businesses needed updates immediately.&lt;/p&gt;

&lt;p&gt;Waiting until the next scheduled pull was not acceptable.&lt;/p&gt;

&lt;p&gt;To solve this, we introduced a product upsert API.&lt;/p&gt;

&lt;p&gt;Now tenants had two options:&lt;/p&gt;

&lt;h3&gt;
  
  
  Pull Model
&lt;/h3&gt;

&lt;p&gt;Commerce Connect fetches products from the provider daily.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Provider → Commerce Connect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Push Model
&lt;/h3&gt;

&lt;p&gt;External systems push product updates directly into Commerce Connect whenever changes occur.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Provider → Product API → Commerce Connect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This hybrid model gave tenants the flexibility they needed while preserving a single source of truth.&lt;/p&gt;




&lt;h1&gt;
  
  
  Operational Reliability and Monitoring
&lt;/h1&gt;

&lt;p&gt;Automation is only valuable if it is reliable.&lt;/p&gt;

&lt;p&gt;Once product synchronization became dependent on external POS APIs, we had to account for situations outside our control:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API credentials changed&lt;/li&gt;
&lt;li&gt;Access tokens expired&lt;/li&gt;
&lt;li&gt;Provider endpoints became unavailable&lt;/li&gt;
&lt;li&gt;Network failures occurred&lt;/li&gt;
&lt;li&gt;Providers returned unexpected response formats&lt;/li&gt;
&lt;li&gt;Product payload validation failures happened&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To ensure synchronization issues never went unnoticed, we built proactive monitoring and alerting into the pipeline.&lt;/p&gt;

&lt;p&gt;Whenever a synchronization task encountered unexpected failures, automated Discord alerts were triggered with relevant diagnostic information, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tenant name&lt;/li&gt;
&lt;li&gt;Provider name&lt;/li&gt;
&lt;li&gt;Failure reason&lt;/li&gt;
&lt;li&gt;API response details&lt;/li&gt;
&lt;li&gt;Task identifiers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This allowed the Customer Success team to immediately investigate whether the issue was caused by invalid credentials, provider-side outages, configuration changes, or data quality problems.&lt;/p&gt;

&lt;p&gt;Instead of discovering synchronization failures days later through customer complaints, the team could identify and resolve issues proactively.&lt;/p&gt;

&lt;p&gt;The result was a much more reliable synchronization platform and significantly reduced operational risk as the number of integrated tenants continued to grow.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Business Impact
&lt;/h1&gt;

&lt;p&gt;From a technical perspective, this wasn't the most complex system I've built.&lt;/p&gt;

&lt;p&gt;There were no distributed consensus algorithms.&lt;/p&gt;

&lt;p&gt;No cutting-edge infrastructure.&lt;/p&gt;

&lt;p&gt;No massive scale challenges.&lt;/p&gt;

&lt;p&gt;But the impact was real.&lt;/p&gt;

&lt;p&gt;Customer onboarding became significantly smoother.&lt;/p&gt;

&lt;p&gt;Customer Success teams no longer spent hours chasing spreadsheets.&lt;/p&gt;

&lt;p&gt;Product synchronization became reliable and automated.&lt;/p&gt;

&lt;p&gt;New POS integrations became faster to implement.&lt;/p&gt;

&lt;p&gt;Operational effort reduced dramatically.&lt;/p&gt;

&lt;p&gt;Most importantly, the business could scale without product data becoming a bottleneck.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmwncbsrhnz7eoz3eqhoi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmwncbsrhnz7eoz3eqhoi.png" alt=" " width="800" height="923"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  What I Learned
&lt;/h1&gt;

&lt;p&gt;One lesson has stayed with me throughout my career:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The most valuable engineering work is not always the most technically sophisticated work.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sometimes the highest-leverage solution is simply removing friction from a process people repeat every day.&lt;/p&gt;

&lt;p&gt;At the time, this felt like a small automation project.&lt;/p&gt;

&lt;p&gt;Today, many of the people using the platform don't even realize the problem it solved.&lt;/p&gt;

&lt;p&gt;That's probably the best outcome an engineering solution can have.&lt;/p&gt;

&lt;p&gt;When a process becomes so smooth that people forget it was ever difficult.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>data</category>
      <category>productivity</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
