Back to Blog
ShopifyWebhookNestJSBullMQ

Shopify-to-Medusa | Phase One of a Progressive Migration

Rather than rebuilding an entire Shopify store from scratch, we adopted a progressive migration strategy. The first phase focuses on building a reliable data synchronization layer using webhooks, asynchronous workers, scheduled snapshots, retry mechanisms, dead letter queues, and message replay. This foundation keeps Shopify running while continuously syncing products, customers, and orders into an internal database, enabling future modules to be migrated safely and incrementally.

RaytonX
6 min read

As more e-commerce businesses scale, many teams are re-evaluating their relationship with Shopify. It is an excellent platform to launch a store quickly, but as the business grows, its limitations become increasingly apparent. Recently, we helped a client complete the first phase of migrating from Shopify to a self-controlled backend.


Why Migrate?

There are three common reasons behind this decision.

Limited Customization

Shopify's ecosystem covers most standard e-commerce scenarios, but it becomes restrictive when business requirements diverge from the norm. Custom pricing strategies, complex membership systems, or non-standard fulfillment workflows often require developers to work around platform limitations instead of building actual business value.

Costs Increase with Growth

For smaller stores, Shopify's pricing is easy to justify. However, as GMV grows, transaction fees and Shopify Plus subscription costs become a significant operational expense. More importantly, these costs cannot be optimized through engineering improvements—they are tied directly to the platform itself.

Data Ownership

Products, customers, and orders all reside inside Shopify's infrastructure. While APIs provide access to the data, storage, query performance, and analytical capabilities remain under Shopify's control. Building custom analytics or integrating data into internal systems becomes increasingly difficult when the platform owns the data layer.


Why Start with Data Synchronization?

A common instinct is to rebuild everything and switch over once the new system is complete. While technically possible, this approach carries substantial engineering and business risks. Development cycles become long, deployment becomes an all-or-nothing event, and rollback options are limited.

Instead, we adopted a progressive migration strategy.

Shopify continues handling production traffic while new capabilities are built alongside it. Individual modules are replaced gradually, validated independently, and can be rolled back whenever necessary.

The prerequisite for this strategy is data consistency.

Before replacing any business module, the internal database must stay synchronized with Shopify. Otherwise, newly migrated services will operate on incomplete or outdated data.

The objective of the first phase is therefore straightforward: establish a reliable synchronization layer that continuously mirrors products, orders, and customers into an internal database without affecting existing Shopify operations.


Architecture Overview

The synchronization layer consists of five independent modules, each solving a specific problem.

Module 1: Webhook Receiver — Respond Fast, Process Later

The Challenge

Whenever data changes, Shopify sends a webhook event and expects a 200 OK response within five seconds. If processing logic such as database operations or data transformation happens directly inside the webhook handler, any downstream delay or failure can cause Shopify to retry the request.

The Solution

The webhook receiver performs only three operations:

  • Validate the HMAC signature
  • Persist the raw webhook payload
  • Push the event into a BullMQ queue

Once these steps are completed, it immediately returns 200 OK, typically within a few milliseconds.

Why This Design?

Receiving events and processing events are two separate responsibilities.

The receiver focuses only on accepting data reliably, while business logic is handled asynchronously by downstream workers. Message queues decouple both layers, allowing webhook throughput and processing complexity to scale independently.


Module 2: Active Polling + Passive Webhooks

The Challenge

Relying solely on webhooks introduces an unavoidable risk.

If a webhook is lost due to network issues, service downtime, or temporary receiver failures, the corresponding data change disappears without notice.

Inventory synchronization is particularly sensitive because stock changes frequently and requires high accuracy.

The Solution

Alongside webhook-driven updates, a scheduled inventory snapshot service periodically retrieves inventory data through Shopify APIs and synchronizes it with the internal database.

Webhooks provide near real-time incremental updates, while scheduled snapshots periodically reconcile the entire dataset.

Why This Design?

No single synchronization mechanism is perfectly reliable.

Webhooks maximize real-time responsiveness, while periodic snapshots guarantee long-term consistency. Combining both provides a much more resilient synchronization strategy for production systems.


Module 3: Asynchronous Workers

The Challenge

After events enter the queue, they still need to be transformed, mapped, persisted, and potentially propagated to downstream systems.

These operations may take time and occasionally fail. Running them inside the webhook receiver would compromise the stability of the entire ingestion pipeline.

The Solution

Dedicated BullMQ workers consume queued events asynchronously.

Separate queues are maintained for products, customers, and orders, with each category handled by its own worker.

Why This Design?

This separation enables independent deployment and scaling.

A surge in order volume only requires scaling order workers, while product and customer processing remain unaffected. Likewise, deploying fixes to one worker does not interrupt webhook ingestion or unrelated workloads.


Module 4: Retry Strategy and Dead Letter Queue

The Challenge

Failures are inevitable.

Temporary database outages, API timeouts, unexpected edge cases, or software bugs can all cause message processing to fail.

How the system handles failures determines its production reliability.

The Solution

Failed jobs enter an automatic retry process using exponential backoff.

Instead of retrying immediately, each retry waits progressively longer, reducing pressure on downstream services during transient failures.

Once the retry limit is exceeded, the message is moved into a Dead Letter Queue (DLQ).

The original payload and error details remain available for investigation, allowing engineers to fix the issue and replay the message later.

Why This Design?

Retries handle temporary failures.

Dead Letter Queues handle persistent failures.

Together they ensure that no message is silently lost while providing a controlled recovery path.


Module 5: State Management and Message Replay

The Challenge

Queue systems are naturally opaque.

When production incidents occur, teams need answers to questions like:

  • Which messages have been processed?
  • Which ones failed?
  • Why did they fail?
  • Which business data was affected?

Without visibility, troubleshooting becomes difficult and time-consuming.

The Solution

Every message maintains a lifecycle state inside the database:

pending → processing → done / failed

Workers update these states throughout processing, while failure reasons are recorded alongside the message.

This state model enables selective message replay. After fixing a bug or resolving an external dependency, failed messages can be filtered and resubmitted without impacting successfully processed events.

Why This Design?

Observability is a fundamental capability of production systems.

Being able to quickly identify failed events, understand their causes, and replay only the affected messages dramatically reduces recovery time and operational risk.


Conclusion

These five modules work together to solve one fundamental problem:

Building a reliable, observable, and recoverable synchronization layer without disrupting existing Shopify operations.

This synchronization layer serves as the foundation of a progressive migration strategy.

Once the data layer is stable, business modules can be replaced incrementally with confidence, enabling a gradual transition from Shopify toward a fully self-controlled backend.