How I untangle legacy scripts into dedicated Function APIs

When Shopify retired Ruby Scripts, the biggest architectural shift I had to explain to merchants was that one monolithic script no longer mutates everything in the cart at once. Instead, Shopify Functions separate business logic by domain: Discount Functions, Delivery Customization Functions, Payment Customization Functions, and Cart Transform Functions. Before I touch the keyboard, I sit down with the operations team to document the exact inputs, customer eligibility rules, and intended outputs.

Because Functions compile to WebAssembly and run inside Shopify’s infrastructure under strict instruction and payload budgets, I keep input GraphQL queries minimal and store dynamic promo thresholds inside configuration metafields so merchants can adjust campaigns without a code deploy.

Why I isolate the pure decision logic for unit testing

Debugging a compiled WebAssembly binary inside a live checkout is painful if your core math is mixed up with GraphQL boilerplate. My habit is to write the core business rule as a pure, zero-dependency function that operates on integer minor currency units (while respecting zero-decimal currencies like JPY or IDR in the adapter layer).

In the example below, I validate every input defensively and sort tiers deterministically so that unit tests can verify edge cases in milliseconds before the Function adapter formats the final Shopify operation.

CONCEPT / RULES
  1. 01Eligible?
  2. 02Yes → select tier
  3. 03No → no discount
Example only: evaluate eligibility before selecting a configured tier or returning no discount.
JAVASCRIPT
export function chooseDiscount({eligible, subtotalMinor, tiers}) {
  if (!Number.isSafeInteger(subtotalMinor) || subtotalMinor < 0) {
    throw new TypeError("Invalid subtotal");
  }
  for (const tier of tiers) {
    if (!Number.isSafeInteger(tier.minimumMinor) || tier.minimumMinor < 0 ||
        !Number.isInteger(tier.percent) || tier.percent < 0 || tier.percent > 100) {
      throw new TypeError("Invalid tier");
    }
  }
  if (!eligible) return 0;
  const matching = [...tiers]
    .sort((a, b) => b.minimumMinor - a.minimumMinor)
    .find(tier => subtotalMinor >= tier.minimumMinor);
  return matching?.percent ?? 0;
}

My pure tier-decision helper—isolated from the GraphQL adapter so every threshold boundary can be unit-tested.

How I validate a Function before production release

  1. I run automated unit tests covering eligible buyers, guest shoppers, missing metafields, and empty carts.
  2. I test boundary values at `minimumMinor - 1`, exact `minimumMinor`, and `minimumMinor + 1` for every tier.
  3. I verify discount combination rules and multi-currency conversion behavior in a development store.
  4. I validate generated GraphQL input/output types against the exact pinned Shopify Function API version.
  5. I run `shopify app function replay` against real test cart traces and document recovery procedures before enabling the rule in production.

Sources & review

Written from my perspective as a Shopify software engineer. Unverified execution-time promises remain excluded; the JavaScript snippet demonstrates pure, testable tier logic separate from the generated Shopify Function schema wrapper.

Content reviewed 2026-10-25. Written in the first person by Mifan Twan Ardana. No production client benchmark is claimed.