"Can you build us a custom discount?" is one of the most common things merchants ask me, and the honest answer is always "yes, but where it should live depends on a few things." Cart logic, shipping rules, and discounts can run as a Shopify Function, inside a full app, or as a combination of both. Picking wrong means either a frustrating ceiling later or a lot of machinery you didn't need.
Here's how I actually decide.
What a Function is good at
A Shopify Function is a small piece of logic that runs inside Shopify's own systems: the discount engine, the shipping rate calculation, the checkout. It's fast, it runs server-side at Shopify's scale, and it doesn't need infrastructure you have to host and babysit.
That makes Functions excellent for rules: "spend over £x in this collection, get y% off," "hide express shipping for these postcodes," "reorder payment methods for B2B customers." Deterministic logic, decided at checkout, with inputs Shopify already has.
Where a Function hits its ceiling
Functions are deliberately constrained. They have tight execution limits, they can't call out to your own database or a third-party API mid-checkout, and they can't hold long-running state. The moment your rule needs to remember something, fetch something external, or show a rich admin UI, you've outgrown a Function on its own.
That's the signal you need an app around it.
A rough decision table
This is the shorthand I run through in my head:
| What you need | Function alone | App (+ Function) |
|---|---|---|
| Discount/shipping rule from cart data | ✅ | overkill |
| Merchant-configurable settings UI | ⚠️ limited | ✅ |
| Lookups against your own DB or an API | ❌ | ✅ |
| Logic that runs at checkout, fast | ✅ | ✅ (Function does the checkout bit) |
| Reporting, history, webhooks, jobs | ❌ | ✅ |
The pattern in that last column matters: it's rarely "app instead of Function." It's an app that owns the configuration, data, and UI, with a Function as the thin, fast piece that actually runs at checkout. The app is the brain; the Function is the reflex.
A concrete example
Say you want tiered wholesale pricing that depends on a customer's lifetime spend.
- The spend total lives in your data and changes over time → that's app territory (a webhook keeps a tally, a job recalculates tiers).
- The "apply the right tier at checkout" step has to be instant and run inside Shopify → that's a Function, fed the customer's current tier as input.
Neither piece can do the job alone. Together they're clean.
The trade-off worth weighing
A Function is cheaper to build and has almost nothing to maintain: no servers, no uptime to worry about. An app is more capable but it's a thing you now own: hosting, updates, Shopify API version bumps twice a year.
So my default is: push as much as possible into a Function, and only add an app for the parts that genuinely can't live there. Less surface area, less to break, less to pay for over time.
If you've got a rule in mind and you're not sure which side of the line it falls on, send me the details. Working out the boundary is half the value.
Written by Charles Marsh, who runs EC Web. Get in touch or book a chat.