Arrays, Strings & Hashing
- Recognize arrays, strings, and hashing as the substrate the classic Shopify coding screen leans on most
- Reach for prefix-sum plus a hashmap, frequency counting, and binary-search boundaries on commerce-shaped inputs
- Model merchant data cleanly (ledgers, order lines, shipping tiers) so the interviewer can layer complexity
- Work three reported shapes end to end, each with a tested escalation
Alongside the pair-programming build Shopify is known for, most loops carry a more classic coding round. Expect a live shared editor, your own language, and a medium-difficulty problem you talk through and test as you go. That round is where array, string, and hashing fundamentals surface most directly, and it is the family this lesson covers. Nothing here is exotic. The work is reading merchant-shaped input carefully, picking the one data structure that makes the rule cheap, and keeping your code clean enough that the interviewer wants to add a second requirement on top.
Expect the problem to sound like a feature ticket. Shopify is the commerce platform behind millions of independent stores, so its screens dress the same fundamentals in its own domain. Think an inventory ledger, a run of order line items, or a table of shipping rates by weight. Strip the story down to the pattern fast. A cart is a hashmap. A stock ledger is a prefix sum. Best-sellers is frequency counting plus a sort. Then re-attach the domain in your variable names, because a candidate who writes units_sold and shipping_band is already framing the work around merchants.
How the screen is framed
The bar Shopify states in its own interview guidance is craft over speed: a tested, cleanly approached solution scores higher than a rushed one, even if you never reach the final case. So the screen is not a race to spot an O(n) trick. It is a check that you can model the input into a clean structure, apply the stated rules exactly, and prove correctness with a few of your own assertions before the interviewer asks. Docs and search are usually allowed. A pre-written solution to the exact problem is not.
on_hand and ledger instead of arr and sum. That is the naming of someone who has debugged a real inventory bug, which is exactly why Shopify phrases the problem in stock and ledger terms in the first place.Three tools carry almost every problem in this family. A hashmap of first-seen prefix sums turns "longest window that nets to X" from an O(n squared) scan into one pass. A hashmap of counts turns ranking and deduplication into a tally plus a sort. A binary search on a sorted array turns "which tier does this value fall into" into a boundary search. Learn to hear which one a rule is asking for, and the rest is careful code.
The prefix-sum plus hashmap pattern
The most reported fundamentals problem in this family is the longest subarray that sums to zero. Learn it as a template, because the same shape answers a whole cluster of "longest window that nets to a target" questions. The idea is that a running prefix sum encodes the whole history in one number. If the same prefix value shows up at two positions, every element between them cancels out to zero. So you record the earliest index where each prefix value appeared, and whenever you see that value again, the span between them is a candidate answer.
def longest_zero_sum_window(deltas): seen = {0: -1} # prefix sum -> earliest index it appeared (seed the empty prefix) running = 0 best = 0 for i, delta in enumerate(deltas): running += delta if running in seen: best = max(best, i - seen[running]) # same prefix twice => the gap nets to zero else: seen[running] = i # keep only the FIRST index, for the longest span return bestTwo details decide whether this works. Seeding the map with {0: -1} handles a window that starts at index zero, because the empty prefix has sum zero at position minus one. And you must store only the first index a prefix value appears at, never overwrite it, because the earliest position gives the widest span. Overwriting is the most common bug, and it quietly returns a shorter window that still passes the easy tests.
running - target. Problem one below works that escalation in full.Asked at Shopify
Three reported shapes, one per tool. Each starts as a clean fundamentals problem and carries the kind of escalation a Shopify interviewer layers on once the first version passes. Try each in the editor, then reveal the walkthrough.
Longest Zero-Sum Window: Prefix Sums
Problem
Return the LENGTH of the longest such run, or
0 if no non-empty run nets to zero.Input: deltas = [1, 2, -2, 4, -4]
Output: 4
The run [2, -2, 4, -4] nets to zero and has length 4. The whole array nets to 1, so it does not count.
Input: deltas = [4, -4, 3, -1, -2]
Output: 5
The entire array nets to zero: 4 - 4 + 3 - 1 - 2 = 0, so the answer is its full length 5.
Input: deltas = [5]
Output: 0
A single non-zero adjustment can never net to zero, so there is no qualifying run.
Track a running prefix sum. Seed a map with {0: -1} and record the first index each prefix value appears at. When a prefix value repeats, the elements between the two positions net to zero, so the span is a candidate answer.
Longest Zero-Sum Window: Prefix Sums
Click "Run" to execute your code
Test Cases
[[1, 2, -2, 4, -4]]Expected: 4[[4, -4, 3, -1, -2]]Expected: 5[[5]]Expected: 0[[]]Expected: 0[[0]]Expected: 1[[2, -2]]Expected: 2Top Best-Sellers: Count and Rank
Problem
sku:quantity, where sku is a product code and quantity is a positive integer number of units on that line. The same sku can appear on many lines.Return the top
n best-selling skus by total units sold, most units first. Break ties between skus with equal units by sku in ascending (alphabetical) order, so the output is deterministic. If fewer than n distinct skus exist, return all of them.Input: lines = ["shirt:2", "mug:5", "shirt:3", "hat:5", "mug:1"] n = 2
Output: ["mug", "hat"]
Totals are mug 6, shirt 5, hat 5. mug leads. shirt and hat tie at 5, so hat wins the tiebreak alphabetically, giving ["mug", "hat"].
Input: lines = ["shirt:2", "mug:5", "shirt:3", "hat:5", "mug:1"] n = 1
Output: ["mug"]
mug has the most units at 6, so the single best-seller is mug.
Tally first, rank second. Parse each line into (sku, quantity) and sum quantities per sku in a hashmap. Then sort the skus by units descending, using the sku itself ascending as the tiebreaker, and take the first n.
Top Best-Sellers: Count and Rank
Click "Run" to execute your code
Test Cases
["shirt:2", "mug:5", "shirt:3", "hat:5", "mug:1"], 2Expected: ["mug", "hat"]["shirt:2", "mug:5", "shirt:3", "hat:5", "mug:1"], 1Expected: ["mug"]["shirt:2", "mug:5", "shirt:3", "hat:5", "mug:1"], 9Expected: ["mug", "hat", "shirt"][], 3Expected: []["a:1", "b:1"], 2Expected: ["a", "b"]Shipping-Band Lookup: Binary Search
Problem
thresholds, a strictly increasing list of the minimum order weight for each band, and rates, the shipping cost in cents for each band, aligned by index. An order falls into the highest band whose threshold is less than or equal to its weight.Given an order weight, return the shipping cost that applies. If the weight is below the first threshold, no band applies, so return
None.Input: thresholds = [0, 5, 20, 50] rates = [300, 800, 1500, 3000] weight = 12
Output: 800
Bands with a threshold at or below 12 are the ones at 0 and 5. The highest is index 1 (threshold 5), so the rate is 800.
Input: thresholds = [0, 5, 20, 50] rates = [300, 800, 1500, 3000] weight = 50
Output: 3000
The weight 50 exactly meets the top threshold, so it lands in the last band at rate 3000.
Binary search for a boundary. Find the highest index whose threshold is less than or equal to the weight. Track the best qualifying index and move right when a band qualifies, left when it does not.
Shipping-Band Lookup: Binary Search
Click "Run" to execute your code
Test Cases
thresholds=[0,5,20,50], rates=[300,800,1500,3000], weight=12Expected: 800thresholds=[0,5,20,50], rates=[300,800,1500,3000], weight=0Expected: 300thresholds=[0,5,20,50], rates=[300,800,1500,3000], weight=49Expected: 1500thresholds=[0,5,20,50], rates=[300,800,1500,3000], weight=50Expected: 3000thresholds=[5,20,50], rates=[800,1500,3000], weight=3Expected: NoneOrder Total: Stack Discounts by Precedence, Then Tax by Region
Problem
sku, a unit_cents price, and a qty. Alongside it come a list of discount rules, the buyer's region code, and a tax_rates map from region code to a tax rate in basis points, so 1300 means 13 percent. Compute the order total in integer cents.Each rule is an object with a
scope of "item" or "order", a kind of "percent" or "flat", and an integer value. An item rule also names the sku it applies to and touches only lines with that sku. A rule naming a sku that is not in the order does nothing.Apply the rules in this precedence:
- Item rules first, line by line. Within a line, every percent rule applies before any flat rule, each kind in the order the rules were given. A percent rule takes
floor(amount * value / 100) cents off the running line amount, so two percent rules on one line compound, and 20 then 10 leaves 72 percent of the line. A flat item rule takes value cents off every unit on the line.- Then sum the lines into a subtotal and apply the order rules the same way, percent first and flat second, with a flat order rule taking
value cents off the subtotal once.- A line amount or the subtotal never drops below zero.
- Finally add tax of
floor(subtotal * rate / 10000) using the rate for region. A region with no entry in the map pays no tax.Return the discounted subtotal plus tax as an integer number of cents.
Input: lines = [{"sku": "shirt", "unit_cents": 2500, "qty": 2}, {"sku": "mug", "unit_cents": 900, "qty": 3}] rules = [{"scope": "order", "kind": "flat", "value": 500}, {"scope": "item", "sku": "mug", "kind": "percent", "value": 20}] region = "ON" tax_rates = {"ON": 1300, "BC": 1200}
Output: 7525
Shirts total 5000 with no item rule. Mugs start at 2700 and the 20 percent rule takes 540 off, leaving 2160. The subtotal 7160 then loses the flat 500 for 6660, even though that rule was listed first. Ontario tax is floor(6660 * 1300 / 10000) = 865, so the total is 7525.
Input: lines = [{"sku": "mug", "unit_cents": 1000, "qty": 1}] rules = [{"scope": "item", "sku": "mug", "kind": "percent", "value": 20}, {"scope": "item", "sku": "mug", "kind": "percent", "value": 10}] region = "ZZ" tax_rates = {"ON": 1300}
Output: 720
The first rule takes 200 off for 800, then the second takes floor(800 * 10 / 100) = 80 off for 720. The two rules compound to 28 percent off, where adding them would have given 30. Region ZZ has no rate, so no tax is added.
Input: lines = [{"sku": "mug", "unit_cents": 900, "qty": 1}] rules = [{"scope": "order", "kind": "flat", "value": 2000}] region = "ON" tax_rates = {"ON": 1300}
Output: 0
The flat 2000 exceeds the 900 subtotal, which floors at zero. Tax on zero is zero, so the order total is 0.
Treat the rules as data and run the same two-pass routine at each scope. Filter to the rules that apply: item rules by sku per line, then the order rules. Apply every percent rule, then every flat rule with a floor at zero, and only then look up the region rate. Integer cents and floor division throughout.
Order Total: Stack Discounts by Precedence, Then Tax by Region
Click "Run" to execute your code
Test Cases
[[{"sku": "shirt", "unit_cents": 2500, "qty": 2}, {"sku": "mug", "unit_cents": 900, "qty": 3}], [{"scope": "order", "kind": "flat", "value": 500}, {"scope": "item", "sku": "mug", "kind": "percent", "value": 20}], "ON", {"ON": 1300, "BC": 1200}]Expected: 7525[[{"sku": "mug", "unit_cents": 1000, "qty": 1}], [{"scope": "item", "sku": "mug", "kind": "percent", "value": 20}, {"scope": "item", "sku": "mug", "kind": "percent", "value": 10}], "ZZ", {"ON": 1300}]Expected: 720[[{"sku": "mug", "unit_cents": 900, "qty": 1}], [{"scope": "order", "kind": "flat", "value": 2000}], "ON", {"ON": 1300}]Expected: 0[[{"sku": "mug", "unit_cents": 1000, "qty": 1}], [{"scope": "order", "kind": "flat", "value": 500}, {"scope": "order", "kind": "percent", "value": 20}], "ON", {"ON": 1300}]Expected: 339[[{"sku": "shirt", "unit_cents": 2500, "qty": 2}, {"sku": "mug", "unit_cents": 900, "qty": 3}], [{"scope": "order", "kind": "percent", "value": 10}, {"scope": "item", "sku": "mug", "kind": "flat", "value": 100}], "BC", {"ON": 1300, "BC": 1200}]Expected: 7459[[{"sku": "mug", "unit_cents": 900, "qty": 1}], [{"scope": "item", "sku": "hat", "kind": "percent", "value": 50}], "ON", {"ON": 1300}]Expected: 1017[[], [{"scope": "order", "kind": "flat", "value": 500}], "ON", {"ON": 1300}]Expected: 0[[{"sku": "pen", "unit_cents": 333, "qty": 3}], [{"scope": "item", "sku": "pen", "kind": "percent", "value": 15}], "NY", {"NY": 8875}]Expected: 1604[[{"sku": "mug", "unit_cents": 200, "qty": 3}, {"sku": "hat", "unit_cents": 1000, "qty": 1}], [{"scope": "item", "sku": "mug", "kind": "flat", "value": 300}], "ZZ", {"ON": 1300}]Expected: 1000Two Sum: One Pass with a Complement Map
Problem
prices, the price in cents of each line item in their cart in cart order, and balance, the remaining value on the card in cents. Find two different line items whose prices add up to balance exactly.Return the two indices as a list
[i, j] with i < j. If no such pair of line items exists, return an empty list. A line item cannot be paired with itself, and you may assume at most one qualifying pair exists.Input: prices = [1500, 2500, 800, 1200] balance = 2000
Output: [2, 3]
The items at indices 2 and 3 cost 800 + 1200 = 2000, which uses the whole balance. No other pair reaches 2000.
Input: prices = [1000, 250, 750] balance = 1000
Output: [1, 2]
Index 0 alone equals the balance, but one item is not a pair. Indices 1 and 2 cost 250 + 750 = 1000.
Input: prices = [500] balance = 1000
Output: []
A single 500-cent item would need a second 500-cent item, and there is none. It cannot pair with itself.
Walk the prices once with a map from price to index. For each item compute the complement, balance minus price, and check the map for it before inserting the current item. Checking before inserting means an item never matches itself.
Two Sum: One Pass with a Complement Map
Click "Run" to execute your code
Test Cases
[[1500, 2500, 800, 1200], 2000]Expected: [2, 3][[1500, 2500, 800, 1200], 4000]Expected: [0, 1][[1000, 250, 750], 1000]Expected: [1, 2][[500], 1000]Expected: [][[500, 500], 1000]Expected: [0, 1][[300, 700, 400], 2000]Expected: [][[], 100]Expected: [][[0, 0], 0]Expected: [0, 1]Order Fraud Flags: Aggregate per Key and Threshold
Problem
order_id,card,address,timestamp,amount_cents. card is the fingerprint of the payment card, address is the shipping address, timestamp is a non-negative integer number of seconds, and amount_cents is the order total in cents. Order ids are unique and no field contains a comma.Flag orders with three rules, each with its own threshold.
address-spread: a card that ships to more than max_addresses distinct addresses across the batch. Every order on that card is flagged.velocity: time is split into fixed buckets of window_seconds starting at zero, so an order's bucket is timestamp // window_seconds. When a card has more than max_per_window orders in one bucket, every order in that bucket is flagged.amount-cap: any single order whose amount_cents is greater than max_amount_cents.Return a list of
[order_id, rule] pairs, one for every rule an order trips, ordered by the order's position in the input and then by rule in the order given above.Input: lines = ["o1,card_a,addr_1,0,4500", "o2,card_a,addr_2,20,1200", "o3,card_a,addr_3,40,800", "o4,card_b,addr_9,100,72000", "o5,card_c,addr_5,200,3000"] max_addresses = 2, max_per_window = 3, window_seconds = 60, max_amount_cents = 50000
Output: [["o1", "address-spread"], ["o2", "address-spread"], ["o3", "address-spread"], ["o4", "amount-cap"]]
card_a ships to three distinct addresses, one more than allowed, so all three of its orders are flagged. Its three orders in bucket 0 do not exceed the limit of three. o4 is 72000 cents, above the 50000 cap. o5 trips nothing.
Input: lines = ["o1,card_v,addr_1,0,1000", "o2,card_v,addr_1,10,1000", "o3,card_v,addr_1,20,1000", "o4,card_v,addr_1,30,1000", "o5,card_v,addr_1,70,1000"] max_addresses = 2, max_per_window = 3, window_seconds = 60, max_amount_cents = 50000
Output: [["o1", "velocity"], ["o2", "velocity"], ["o3", "velocity"], ["o4", "velocity"]]
Bucket 0 covers timestamps 0 through 59 and holds four orders on card_v, one over the limit, so all four are flagged. o5 at 70 sits alone in bucket 1. One address and small amounts, so no other rule fires.
Parse each line into an order record once. Then every rule is the same move: group orders by a key (the card, or the card plus a time bucket), measure the group, compare to its threshold, and flag. Keep each rule in its own function that returns a set of flagged ids, and merge at the end.
Order Fraud Flags: Aggregate per Key and Threshold
Click "Run" to execute your code
Test Cases
[["o1,card_a,addr_1,0,4500", "o2,card_a,addr_2,20,1200", "o3,card_a,addr_3,40,800", "o4,card_b,addr_9,100,72000", "o5,card_c,addr_5,200,3000"], 2, 3, 60, 50000]Expected: [["o1", "address-spread"], ["o2", "address-spread"], ["o3", "address-spread"], ["o4", "amount-cap"]][["o1,card_v,addr_1,0,1000", "o2,card_v,addr_1,10,1000", "o3,card_v,addr_1,20,1000", "o4,card_v,addr_1,30,1000", "o5,card_v,addr_1,70,1000"], 2, 3, 60, 50000]Expected: [["o1", "velocity"], ["o2", "velocity"], ["o3", "velocity"], ["o4", "velocity"]][["o1,card_x,addr_1,40,100", "o2,card_x,addr_1,50,100", "o3,card_x,addr_1,59,100", "o4,card_x,addr_1,60,100", "o5,card_x,addr_1,61,100"], 2, 3, 60, 50000]Expected: [][["o1,card_u,addr_1,130,100", "o2,card_u,addr_1,5,100", "o3,card_u,addr_1,125,100", "o4,card_u,addr_1,70,100", "o5,card_u,addr_1,100,100", "o6,card_u,addr_1,140,100", "o7,card_u,addr_1,170,100"], 2, 3, 60, 50000]Expected: [["o1", "velocity"], ["o3", "velocity"], ["o6", "velocity"], ["o7", "velocity"]][["o1,card_m,addr_1,0,100", "o2,card_m,addr_2,5,100", "o3,card_m,addr_3,10,99999"], 2, 3, 60, 50000]Expected: [["o1", "address-spread"], ["o2", "address-spread"], ["o3", "address-spread"], ["o3", "amount-cap"]][[], 2, 3, 60, 50000]Expected: [][["o1,card_e,addr_1,0,50000", "o2,card_e,addr_2,100,50000", "o3,card_e,addr_1,200,50000"], 2, 3, 60, 50000]Expected: [][["o1,card_p,addr_1,0,500", "o2,card_p,addr_1,5,500", "o3,card_p,addr_2,100,1500"], 1, 1, 10, 1000]Expected: [["o1", "address-spread"], ["o1", "velocity"], ["o2", "address-spread"], ["o2", "velocity"], ["o3", "address-spread"], ["o3", "amount-cap"]][["o1,card_a,addr_1,0,100", "o2,card_b,addr_1,0,100", "o3,card_a,addr_2,1,100", "o4,card_b,addr_2,1,100", "o5,card_c,addr_3,2,100", "o6,card_a,addr_3,3,100"], 2, 3, 60, 50000]Expected: [["o1", "address-spread"], ["o3", "address-spread"], ["o6", "address-spread"]]Where the screen goes wrong
The ways candidates lose this round are rarely about missing the algorithm. They are about parsing sloppily, skipping the tests, or leaving output nondeterministic, and each maps straight onto what Shopify grades.
Do
- Name the pattern before coding: prefix sum, count map, or boundary search.
- Parse defensively, stripping spaces and converting quantities to int at the point you read them.
- Give every sort an explicit compound key so ties resolve the same way every run.
- Write and run a handful of assertions the moment the first version works, covering the empty and boundary cases.
- Keep the domain in your naming, so on_hand, units_sold, and shipping_band read like real code.
Don't
- Overwrite a first-seen prefix index and quietly return a shorter window.
- Sort by count alone and let equal-count items land in dict-iteration order.
- Reach for float on money or weight when integers keep the comparisons exact.
- Skip the underweight or empty-input edge and wait for the interviewer to surface it.
- Chase an O(n) micro-optimization before the code is correct and readable.
Hear the structure under the story, then keep the story in your names and your tests. A clean, tested tally or prefix-sum pass that you can extend when the interviewer adds a requirement beats a clever one-liner that drops the boundary case. That is the craft-over-speed bar Shopify states outright, and this family of problems is where it is easiest to show.
Key Takeaways
- ✓The classic Shopify coding screen leans hardest on arrays, strings, and hashing, with the input handed to you as ledgers, order lines, and shipping tiers.
- ✓Three tools carry the family: a first-seen prefix-sum map for zero-sum and target windows, a count map for ranking, and a boundary binary search for tiers.
- ✓Model the input cleanly, apply the stated rule exactly, and make output deterministic with explicit sort keys. Craft and tests outrank speed here.
- ✓Structure the first version so the interviewer can layer a requirement on top, then prove each step with assertions you write yourself.
Shopify interview guideKnow what Shopify asks before you walk in.
26 coding · 5 system design · 21 behavioral questions
Get their questions first.
InterviewLegend is independent and not affiliated with or endorsed by any company it covers.