Skip to content
Ecommerce · SaaS architecture

Designing order systems in a multi-tenant ecommerce SaaS

Lessons from years of engineering order, payment and fulfillment workflows in Shoprocket, a multi-tenant Laravel ecommerce SaaS serving thousands of merchant stores.

Indesh Prinja 12 min read

Based on production work on Shoprocket . This is engineering perspective, not product documentation.

System map
Light editorial diagram of order flow: checkout, order, payment and fulfillment steps with icons
Checkout, payment, inventory, tax, shipping and fulfillment all converge on one order record. Each layer can move at its own pace.

From the outside, ecommerce looks like products, a cart, checkout, and an order list. On Shoprocket, where thousands of merchants share one platform, the difficulty is everything that happens after checkout: payments that settle asynchronously, inventory that depends on product type, tax and shipping frozen at purchase time, and integrations that retry or arrive out of order.

I worked on order-related systems there for years. These notes are about design choices that only show up once a commerce product is live, connected to gateways and APIs, and expected to keep working while it grows.

Every path needs a store context

In a single-store app, tenant scope is implicit. In SaaS, a dashboard route, queue job, webhook handler and API token lookup all have to resolve one store before order logic runs. Shared queues and provider callbacks that carry only a gateway reference make that easy to miss on one path.

I wrote about isolation patterns in more detail in Tenant isolation in multi-tenant Laravel SaaS. On orders, the short version is the same: resolve the store first, then mutate state.

An order is a state machine, not one status column

A single status field works until payment, fulfillment, refunds and partial shipments need to move at different speeds while still describing one purchase.

In production, an order ties together line items and price snapshots, billing details frozen at checkout, payment events, inventory movement, tax and shipping choices, and fulfillment updates from carriers or manual workflows. Those layers do not advance in lockstep. A webhook can lag. A merchant can ship one line item today and another next week. A refund can be partial.

We kept a simple status for merchants while the backend tracked finer states and allowed only sensible transitions between them.

Checkout ending is not payment ending

Checkout is UX. Payment is an async conversation with Stripe, PayPal, PayU, crypto providers and others, each with different callbacks and timing. Order creation cannot assume a successful redirect means captured funds.

Shoprocket's payment layer recorded intent, verified webhooks, deduplicated retries, and reconciled when local and provider state diverged. I went deeper on that in Handling payment webhooks in production SaaS.

Inventory commits with the order, not the cart

Carts are speculative. Orders are commitments. Holds, oversell rules for digital goods, and variant-level quantity all need to bind when the order becomes real, not while someone is still browsing.

Multi-tenant catalogs mix physical goods, downloads, services, licence keys, donations and subscriptions. Each type changes what fulfilled means. Checkout should not become a wall of conditionals; order creation should call product-type policies that know how to reserve, skip or roll back inventory when payment fails.

Snapshot tax and shipping on the order

Merchants configure zones and rates in the dashboard. Customers see the result once at checkout. The chosen tax breakdown and shipping method still have to live on the order record for disputes, refunds and reporting, especially when the catalog changes the next day or the platform serves multiple regions and currencies.

Integrations will misbehave

Public APIs, Zapier and inbound webhooks turn the order system into a hub other systems poke asynchronously. They retry, duplicate and arrive late. We stored raw payloads, verified signatures, made handlers idempotent, pushed slow work to queues, and surfaced failures to merchants in language they could act on.

The dashboard should simplify without lying

Merchants ask practical questions: was I paid, what do I ship, can I refund this line. They do not think in webhook event names. The orders dashboard is where backend state becomes daily work.

Merchant operations
Shoprocket merchant dashboard showing the orders list and an individual order detail view
Lists, filters, detail screens and fulfillment actions. This is where abstract state machines meet merchant expectations.

When the UI says paid but capture is still pending, support volume jumps. A clear primary status plus access to underlying payment and fulfillment detail when something breaks saves everyone time.

A few rules I would keep

  1. Model orders as coordinated subsystems, not one field.
  2. Resolve tenant context before mutations in jobs and webhooks.
  3. Treat gateways as async collaborators, not checkout side effects.
  4. Freeze commercial facts at order creation.
  5. Invest in idempotency, reconciliation and visible failure states early.

Commerce SaaS gets harder as it succeeds. More merchants means more edge cases on the same paths that worked for the first hundred stores. Orders are where that pressure shows up first.