After tightening University Recruiting in 2022, Shopify is reopening the New Grad and Early Career pipeline for the 2026 cycle — but the OA has shifted from "one LC Medium" to a business-driven, real-world coding test. This article uses 1point3acres, Reddit r/cscareerquestionsCAD, and Glassdoor data from 2026-03 to 2026-05 to lay out the platforms, question types, scoring rubric, and onsite handoff for Shopify OA.
Shopify OA at a Glance
| Dimension | Detail |
|---|---|
| Platform | CodeSubmit (mainly Backend) / HackerRank (occasional Full-Stack) |
| Duration | 90-120 minutes (Backend 90 / Full-Stack 120) |
| Questions | 1-2 open-ended programming tasks (not LC-style) |
| Difficulty | LC Medium algorithms + business modeling + test coverage |
| Pass Bar | All hidden tests pass + code-review score ≥ 7/10 |
| Feedback Window | 5-14 days |
| Next Round | Life Story behavioral + Technical pair programming |
What makes Shopify different: the OA is not a pure algorithm test — it simulates real product work. Senior Engineers grade your submission like a PR review: readability, error handling, and test organization all count. One-line lambda golfing won't earn points; it loses them.
Question 1: Cart Inventory State Machine
Problem
Implement a minimal Cart class that supports:
add_item(product_id, qty)— adds the item; if inventory is insufficient return"OUT_OF_STOCK", otherwise return the current cart totalremove_item(product_id, qty)— removes the item; if the cart doesn't contain that product return"NOT_FOUND"apply_discount(code)— applies a discount code; the codeSAVE10only works when the cart subtotal is ≥ $100checkout()— atomically deducts inventory and clears the cart; if any item is short at checkout, roll back everything and return"INVENTORY_CHANGED"
Inventory and prices are injected into the constructor: Cart(inventory: dict, prices: dict).
Approach
This is not an algorithm problem; it's a software-engineering problem. Shopify's grading criteria:
- Clear state machine: cart has "pending / committing / done" states. Don't deduct inventory until checkout.
- Error codes vs exceptions: the spec wants string returns — don't raise.
- Atomicity: a failing checkout must roll back. The clean pattern is validate first, commit second, not "deduct then refund".
- Testability: inventory and prices are injected via DI so tests can mock them.
Python Solution
from collections import defaultdict
class Cart:
def __init__(self, inventory: dict, prices: dict):
self.inventory = dict(inventory)
self.prices = dict(prices)
self.items = defaultdict(int)
self.discount = 0.0
def _subtotal(self) -> float:
return sum(self.prices[p] * q for p, q in self.items.items())
def add_item(self, product_id, qty):
if self.inventory.get(product_id, 0) < self.items[product_id] + qty:
return "OUT_OF_STOCK"
self.items[product_id] += qty
return round(self._subtotal() * (1 - self.discount), 2)
def remove_item(self, product_id, qty):
if product_id not in self.items:
return "NOT_FOUND"
self.items[product_id] -= qty
if self.items[product_id] <= 0:
del self.items[product_id]
return round(self._subtotal() * (1 - self.discount), 2)
def apply_discount(self, code):
if code == "SAVE10" and self._subtotal() >= 100:
self.discount = 0.10
return "OK"
return "INVALID"
def checkout(self):
# Validate fully before committing — avoid partial deductions
for p, q in self.items.items():
if self.inventory.get(p, 0) < q:
return "INVENTORY_CHANGED"
for p, q in self.items.items():
self.inventory[p] -= q
total = round(self._subtotal() * (1 - self.discount), 2)
self.items.clear()
self.discount = 0.0
return total
Time complexity: O(k) per operation, where k = number of SKUs in the cart Scoring tip: the two-phase checkout (validate + commit) is almost always a guaranteed point. A "deduct then refund" approach loses 2+ code-review points even if hidden tests pass.
Question 2: GraphQL Webhook Idempotency
Problem
Shopify pushes order webhooks (POST) to merchants, but the same event can be retried 2-5 times. Each payload contains:
{ "event_id": "evt_abc", "order_id": 1001, "amount": 250.0, "ts": 1715000000 }
Implement WebhookHandler:
handle(payload) -> str— first time for anevent_idreturns"PROCESSED", subsequent times return"DUPLICATE"revenue_total() -> float— cumulative revenue (deduplicated)- Extra constraint: only keep event IDs from the last 24 hours in memory; older ones can be forgotten
Approach
Classic "idempotency + sliding window":
- Use a
setfor active event IDs and adequeto slide the window revenue_totalmust only count non-duplicates- Don't store
dict[event_id] = tsand scan the whole dict on every call — that's O(n) per call and will TLE on large hidden tests
Python Solution
from collections import deque
WINDOW = 24 * 60 * 60 # 24 hours in seconds
class WebhookHandler:
def __init__(self):
self.seen = set()
self.queue = deque() # (ts, event_id)
self.total = 0.0
def _evict(self, now):
while self.queue and self.queue[0][0] < now - WINDOW:
_, eid = self.queue.popleft()
self.seen.discard(eid)
def handle(self, payload):
eid = payload["event_id"]
ts = payload["ts"]
self._evict(ts)
if eid in self.seen:
return "DUPLICATE"
self.seen.add(eid)
self.queue.append((ts, eid))
self.total += payload["amount"]
return "PROCESSED"
def revenue_total(self):
return round(self.total, 2)
Time complexity: amortized O(1) per handle (each event_id is pushed/popped at most once)
Pitfall: _evict must use the current payload's ts, not time.time(). Hidden tests deliberately send out-of-order timestamps to catch this.
Question 3: Product Tag Search (prefix + fuzzy)
Problem
Implement a product search service. Stores register product tags (e.g. ["organic", "vegan", "gluten-free"]) and the API supports:
index(product_id, tags: list[str])search(query: str) -> list[int]— returns all product_ids that have any tag starting with the query (sorted ascending)top_tags(k: int) -> list[str]— returns the top-k tags by frequency (ties broken alphabetically)
Approach
- Prefix search → Trie (don't substring-scan; hidden tests with 100k products will TLE)
- top-k →
collections.Counter+heapq.nsmallest
Python Solution
from collections import Counter, defaultdict
import heapq
class TagSearch:
def __init__(self):
self.children = defaultdict(dict)
self.tag_to_products = defaultdict(set)
self.tag_count = Counter()
def _insert(self, tag, pid):
node = self.children
for ch in tag:
if ch not in node:
node[ch] = {}
node = node[ch]
node.setdefault("$products", set()).add(pid)
def _collect(self, node, out):
if "$products" in node:
out.update(node["$products"])
for k, v in node.items():
if k != "$products":
self._collect(v, out)
def index(self, pid, tags):
for t in tags:
self.tag_count[t] += 1
self.tag_to_products[t].add(pid)
self._insert(t, pid)
def search(self, query):
node = self.children
for ch in query:
if ch not in node:
return []
node = node[ch]
result = set()
self._collect(node, result)
return sorted(result)
def top_tags(self, k):
return [t for t, _ in heapq.nsmallest(
k, self.tag_count.items(), key=lambda x: (-x[1], x[0]))]
Time complexity: index O(L), search O(L + R), where L = tag length and R = matched products
Prep Strategy
| Priority | Topic | Recommended LeetCode / Resources |
|---|---|---|
| ⭐⭐⭐ | Design / state machines | LC 146, LC 460, LC 1396, LC 355 |
| ⭐⭐⭐ | Trie / prefix tree | LC 208, LC 211, LC 642 |
| ⭐⭐ | Sliding window / deque | LC 239, LC 1438, LC 1004 |
| ⭐⭐ | OOP / test organization | Practice: 80%+ coverage on a mini-cart |
| ⭐ | GraphQL basics | Shopify dev docs - GraphQL Admin API |
Time budgeting: 90 min = 5 read + 10 class design + 35 first pass + 20 tests + 20 debug. Tests are not optional — Shopify's rubric explicitly includes test coverage.
FAQ
Q1: Is Shopify OA on CodeSubmit or HackerRank?
For 2026 Backend roles, mainly CodeSubmit — you download a starter repo, write locally, and git push to submit. Full-Stack and Front-End occasionally use HackerRank, but less than 20% of cases. CodeSubmit gives no hidden-test progress bar; results take 5-14 days.
Q2: With no hidden-test bar, how do I know my CodeSubmit solution is correct?
Write your own unit tests. CodeSubmit READMEs typically expect npm test or pytest to pass. Reviewers grade your test coverage first — this is the Shopify engineering culture. Cover at minimum: happy path, edge cases (empty input), error paths (invalid ID), and concurrency / ordering (when applicable).
Q3: Shopify's backend is Ruby on Rails — must I use Ruby for the OA?
No. CodeSubmit repos usually offer Ruby, Python, JS, Go, and Java starters. Pick the language you know best. Reviewers grade code organization, not language idioms. That said, if your résumé claims Ruby expertise but you submit in Go, be ready to explain at the onsite.
Q4: How long after passing Shopify OA does onsite happen?
Typically 2-3 weeks to a Recruiter Call → Life Story behavioral (45 min) → Technical pair programming (60 min) → Hiring Manager. Life Story is the Shopify signature round — you walk through your entire career, with the interviewer drilling into "why did you leave / join / pivot" at each turn. Prep at least 3 clear inflection-point narratives.
Q5: What's the difference between Shopify's CA and US 2026 Early Career roles?
CA roles (Ottawa / Toronto / Remote-CA) have more headcount and remote is fine. US roles are scarce, and remote-US applicants are often rejected unless they already have work authorization. Comp: New Grad CAD ~$100-115K + RSU; US (after currency adjustment) is moderately higher.
Q6: I failed Shopify OA — how soon can I reapply?
12-month cooldown. Before re-applying, prepare an engineering reflection note: list what you think you missed (tests, error handling, naming) on attempt one. Recruiters occasionally ask you to talk through this — Shopify cares about growth orientation.
Contact
If you're prepping a business-oriented OA like Shopify, Square, or Stripe, the bottleneck is rarely algorithms — it's code organization and PR-grade testing. We've curated 2025-2026 Shopify CodeSubmit real questions (with runnable multi-language starters and the grading rubric) — feel free to reach out.
Add WeChat Coding0201, get the Shopify OA bank and code-organization template.
- Email: [email protected]
- Telegram: @OAVOProxy