DEV Community

Cover image for Building Sub-5ms Ingestion Middleware for ServiceTitan and Jobber: Architecture & Deduplication Deep Dive
KallowayThayer
KallowayThayer

Posted on Edited on

Building Sub-5ms Ingestion Middleware for ServiceTitan and Jobber: Architecture & Deduplication Deep Dive

When you're handling inbound leads for high-end home services, you can't afford generic middleware boilerplate. If a lead hits your pipeline with a messed-up phone number or a duplicate webhook retry from a frantic homeowner spamming a contact button, your CRM gets polluted and dispatch gets a bad number.

I built our ingestion core to run sub-5ms in transient memory, stripping out the fluff before anything touches ServiceTitan or Jobber. Here is how the core loop handles idempotency, schema flattening, and deduplication.

Dual-Hash Idempotency Without Disk Bottlenecks

Hitting a database on every inbound webhook to check for duplicates adds unacceptable latency. Instead, we use an in-memory sliding-window ledger combined with a dual-hash fingerprint generator (DJB2 + FNV-1a):

JavaScript

const generateFingerprint = (str) => {
if (!str || !String(str).replace(/|/g, '').trim()) return null;
const s = String(str);
let djb2 = 5381;
let fnv = 2166136261;
for (let i = 0; i < s.length; i++) {
const char = s.charCodeAt(i);
djb2 = ((djb2 << 5) + djb2) + char;
fnv ^= char;
fnv = Math.imul(fnv, 16777619);
}
return (djb2 >>> 0).toString(16).padStart(8, '0') + '-' + (fnv >>> 0).toString(16).padStart(8, '0');
};

This gives us two layers of protection:

In-Batch Deduplication (seenInBatch): Catches identical payloads arriving simultaneously in the same execution burst.

Sliding TTL Ledger: Tracks recent events with strict time-to-live expiration (86400000ms for events, 3600000ms for contacts) while automatically pruning memory overflow so the heap doesn't bloat.
Enter fullscreen mode Exit fullscreen mode

Flattening Ingress Chaos

Every form builder and ad network sends data differently. Google Lead Forms use user_column_data, Meta Lead Ads use field_data, and custom webhook forms dump nested objects into body or payload.

We flatten all of those disparate schemas into a single normalized payload before mapping them directly into clean, CRM-ready structures:
JavaScript

const allAdapters = {
servicetitan_v2: {
name: fullFormattedName,
type: isCommercialLead ? 'Commercial' : 'Residential',
address: {
street: street || rawAddress,
unit: unit || undefined,
city: city || undefined,
state: state || undefined,
zip: zip || undefined,
country: resolvedCountryIso3
},
contacts: [
...(cleanPhone ? [{ type: 'MobilePhone', value: cleanPhone, memo: phoneExtension ? 'ext ' + phoneExtension : undefined }] : []),
...(hasValidEmail ? [{ type: 'Email', value: cleanEmail }] : [])
]
},
jobber_graphql: {
firstName: firstName,
lastName: lastName,
companyName: resolvedCompany || undefined,
isCompany: Boolean(entityType === 'CORPORATE_OR_SYSTEM' || (isCommercialLead && resolvedCompany)),
phones: cleanPhone ? [{ number: cleanPhone, description: 'MAIN', primary: true }] : [],
emails: hasValidEmail ? [{ address: cleanEmail, description: 'MAIN', primary: true }] : [],
billingAddress: street ? {
street1: street,
street2: unit || undefined,
city: city || undefined,
province: state || undefined,
postalCode: zip || undefined,
country: resolvedCountryIso2
} : undefined
}
};

Edge Validation & DLQ Quarantine

If a lead has a bogus phone number (like 555-555-5555), profanity in the name field, or invalid email syntax, it never makes it to the CRM.

Instead, the pipeline flags the payload and routes it straight to a Dead-Letter Queue (DLQ) quarantine envelope, complete with automatic PII redaction (REDACT_DLQ_PII) so logs remain safe and inspectable.

Check out the full repository and documentation here:
KallowayThayer/KT-pipeline-core on GitHub

Operated strictly as an asynchronous engineering consultancy. No sales calls. kallowaythayer.com

Top comments (2)

Collapse
 
challan116ux profile image
challan116-ux •

The two-layer dedupe split is well judged: in-batch catches the simultaneous burst, and the TTL ledger catches the retry that arrives minutes later, and they really are different problems wearing the same name. The honest trade-off to name is the in-memory part — a restart clears the ledger, so anything redelivered across a deploy falls through to whatever the CRM side tolerates, which makes the receiving system's own duplicate behaviour part of the design whether you planned it or not. Normalizing before anything touches the CRM is the other quiet win here; dedupe on flattened records catches duplicates that raw-payload hashing would miss.

Collapse
 
kallowaythayer profile image
KallowayThayer •

if the heap drops on a rolling restart, redelivered payloads just fall through to the crm anyway.

thats why we treat the in-ram window strictly as a fast filter for webhook storms, while letting jobber or servicetitan handle the hard durability floor. and yeah hashing raw payloads is a trap, if meta or google shifts key order on you, the hash breaks. flattening first is mandatory.