At a glance, B2B order sync is boring: receive a payload, create a Shopify order. In practice it is one of the most failure-rich patterns in commerce engineering, because you are joining three unreliable worlds: an upstream system, your own enrichment data, and Shopify’s GraphQL API, into one business outcome.
I have built SQS-driven Lambdas that validate records, resolve company locations and catalogs, map pricing, create or reconcile orders, and publish results to a response queue. The lesson: if you model this as a happy-path script, production will humble you.
Why “just create the order” fails
A real B2B sync usually needs answers Shopify will not invent for you:
- Which company is this?
- Which company location matches the ship-to zip or external location id?
- Which catalog / price list applies?
- Do line items map to sellable variants at the expected price?
- What if an order with the same internal id already exists?
- What if Shopify returns user errors on some lines but not others?
- What if the Lambda times out after Shopify already created the order?
Each question is a branch. Each branch is an opportunity for silent corruption.
Validation is not ceremony
Zod (or equivalent) schemas at the edge are not bureaucracy. They are how you keep poison out of the domain layer. An SQS message can be truncated, double-delivered, malformed, or partially upgraded by an upstream team that “only changed one field.”
A disciplined handler does this:
- Parse and validate every record.
- Reject bad shapes early with explicit errors.
- Return
batchItemFailuresso SQS retries only the bad apples. - Never let one poison message block an entire batch forever without visibility.
That last point matters. Batch failure semantics are easy to get subtly wrong. Fail the whole batch and healthy orders stall. Swallow errors and you lie to operations.
Enrichment is where domain complexity hides
The interesting logic is rarely “call orderCreate.” It is everything before that:
- repository lookups against Postgres (sessions, location zip maps, company externals)
- ensuring a Shopify Company Location exists (create/update)
- associating catalogs when needed
- fetching price lists and mapping each line
- translating upstream structures into Shopify GraphQL input via mappers
This is why OOP-ish boundaries pay off in Lambdas. A thin lambda-handler plus RecordProcessor, SyncOrder, repositories, and mappers is not over-engineering. It is how you keep a 600-line “god function” from becoming untestable lore.
Handlers should not know Prisma query details. Mappers should not talk to SQS. Shopify clients should not own business retry policy. When those blur, every bugfix becomes archaeology.
Idempotency is the tax you always pay
Queues retry. Networks blip. Operators replay. If your sync is not idempotent, you will create duplicate Shopify orders or fight duplicates with tribal knowledge.
Patterns that help:
- Treat an upstream internal order id as a correlation key.
- Before create, search for an existing Shopify order with that identity.
- If found, decide deliberately: update metafields, cancel-and-replace, or no-op.
- Write result messages that include enough ids for humans and machines to reconcile.
Cancel-and-replace is powerful and dangerous. It can fix drift. It can also destroy history if you are careless about timing, payments, or fulfillments. Complexity here is not academic. It is operational risk.
Partial success is still failure (sometimes)
Shopify GraphQL user errors are nuanced. You may get a response that is neither clean success nor hard crash. Your result queue must distinguish:
- hard infrastructure failure (retry)
- validation / business rejection (do not infinite-retry)
- success with warnings
- success after reconciliation
If everything is retried the same way, you will either hammer Shopify with known-bad payloads or drop legitimate transient failures.
What I optimize for now
When I design order sync, I optimize for explainability:
- Can an engineer read one failed message and know which stage died?
- Can ops tell whether Shopify was touched?
- Can we replay safely?
- Can we test mappers without AWS?
The API call is the easy part. The system around the API call is the product.
