Every Shopify-plus-logistics integration eventually wants the same shape: webhook in, enriched payload out, retried on failure, deduped on success. The first two versions I shipped were stateless adapters that crumbled under traffic. The third — Laravel + Horizon + a tight contract layer — is still running years later.
The contract layer
Every Shopify webhook maps to a named job class that owns one verb: FulfillOrderJob, SyncInventoryJob, RateQuoteJob. No controller action does business logic. Controllers validate shape, dispatch, return 200. Jobs are idempotent by webhook-id, protected by a unique index in Redis.
The queue shape
Three lanes: high (payment, fulfillment), medium (inventory, rates), low (analytics, exports). Horizon balances workers across them. The rule that kept us out of trouble: never let a low-priority job block a high-priority one by sitting in the same worker pool.
What I'd change
I'd move the contract DTOs to Zod-equivalent runtime validators in PHP (spatie/laravel-data has converged on this pattern). Earlier on we trusted Shopify's payload shape. We shouldn't have; a silent schema change in 2024 cost us a day of fulfilment backlog.
