Production win
Webhook Pipeline for Multi-Tenant Commerce Sync
4+ min lag → sub-10s freshness · ~15% Lambda cost reduction
Architecture diagram
Webhook-driven commerce event pipeline
Per-platform adapters normalize commerce webhooks into tenant-safe, replayable, near-real-time processing.
Commerce platforms
10+ upstream event sources
Webhook events
Webhook adapters
Signature validation and normalization
Normalized payload
SQS + DLQ
Backoff, replay, failure isolation
At-least-once delivery
Idempotency layer
UUID keys and duplicate protection
Safe event
Tenant processors
Ordering and throttling per tenant
Ordered updates
Fresh commerce state
Sub-10s data freshness
- Polling waste was removed instead of simply increasing polling frequency.
- DLQ and replay made platform outages recoverable instead of silent data loss.
- Tenant-level throttling kept noisy tenants from affecting shared reliability.
The most interesting part of this one is not the architecture, it is that the problem was invisible until I went looking. The platform ingested orders from more than ten external commerce systems into our OMS, and the path was designed for eventual consistency: an order would eventually sync, so a few minutes of lag was considered fine. It was not fine. On a revenue-critical path, a customer placing an order waited up to roughly five minutes for it to move from "placed" to "processing" and verify on the OMS. Support had normalized it, the complaints were logged as "operational" and handled case by case, so the systemic delay had no owner. The hard part of this project was recognizing that an accepted status quo was quietly costing user experience and revenue, and that it did not have to be that way.
The platforms either already supported webhooks or could. So on the revenue-critical path I made the case to the OMS team to put webhooks on their roadmap, and when the slot came I picked it up and delivered. A shallow fix would have been to poll more often; I did not, because it would have raised cost without changing the shape of the problem.
I don't bet everything on a happy path, so before shipping webhooks I studied how they fail, and the worst case is that a webhook simply never fires. I did not rip polling out. I kept it as a slow safety net, dropped from every five minutes to once an hour. That hourly sweep re-syncs, and because idempotency is enforced at the destination, the OMS, with a deterministic token derived from each order's own identity, replaying an event converges to a single write rather than double-processing (a per-delivery UUID only dedupes the delivery, not a crash mid-pipeline between our database and the OMS), so any miss or drift self-heals at the next reconcile. Webhooks for speed, hourly polling for safety; the system never assumes the webhook arrived.
For the rest of the usual webhook pain, duplicates, out-of-order delivery, one noisy tenant, I built per-platform adapters that normalize each provider's format, idempotent dedup, per-tenant ordering where sequence mattered, dead-letter handling with replay so failures had a recoverable lane instead of silent loss, and per-tenant throttling so one platform could not drown the others. Observability mattered more than usual: once a system behaves close to real time, "it should catch up" stops being a satisfying answer, so the pipeline had to show where an event was, what failed, and what recovery path existed.
The outcome: freshness from 4+ minutes to sub-10 seconds, a user now sees "placed → processing" in about ten seconds, and Lambda spend down roughly 15% as the polling waste disappeared. The trade-off was accepting more reliability engineering in exchange for fresher truth: polling is easier to reason about because you own the schedule, and webhooks give some of that up, so you need the defensive patterns to go with the choice. I think of this less as "I moved to webhooks" and more as "I changed the contract", faster truth over timing ownership, and the first move was not code at all. It was noticing that everyone had quietly agreed to live with a problem.
The decision
Move the fast path to webhooks while retaining an hourly polling reconciliation. Idempotency belongs at the destination so retries, duplicate deliveries, and a missed webhook converge on one order state.
Alternatives considered
- ·Poll more often. Simple, but it raises cost while preserving the same timing and scaling model.
- ·Remove polling after webhooks ship. Cleaner on paper, but a webhook that never arrives becomes silent data loss.
- ·Deduplicate only by delivery UUID. That catches repeated deliveries, not a retry after a partial failure between local persistence and the OMS.
Tech stack
The challenge
Orders from more than ten commerce platforms eventually reached the OMS, but four-plus minutes of polling lag had become an accepted operational problem on a revenue-critical path.
Architecture approach
- ·Built signature-validating adapters for each provider and normalized their events into one ingestion contract.
- ·Used deterministic order identity at the OMS for idempotency, with per-tenant ordering where sequence mattered.
- ·Added a dead-letter and replay path, tenant-level throttling, and event-level observability.
- ·Reduced the polling loop to an hourly safety sweep so state-bearing entities still converge when delivery fails silently.
Results
- Freshness improved from more than four minutes to under ten seconds
- Lambda cost fell by roughly 15%
- Hourly reconciliation retained a bounded recovery path for missing state-bearing events
What I'd do differently at production scale
- ·Webhook signatures, replay windows, and secret rotation need provider-specific runbooks rather than one generic adapter contract.
- ·The reconciliation objective should be measured as convergence time, not presented as a guarantee that no event can ever be lost.
- ·Backpressure and noisy-tenant isolation need load tests at the partition-key level before adding higher-volume platforms.