Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

CODING CHALLENGE · N°69

Re-Engineer a Legacy ETL

Medium FDEInterviewData

You inherit an aggregation nobody can explain and the totals are wrong. Three separate bugs — an exclude-list where the spec wants an include-list, a dedupe key missing a field, and pre-seeded customers that should never appear. The FDE re-engineering round, with hidden tests.

The problem

A customer’s reporting pipeline aggregates order rows into a per-customer revenue total. The rows come from a join that duplicates a row per shipment leg, so the same order_id can appear several times for one customer with the same amount. Implement summarise(rows) so that: only rows whose status is "shipped" or "delivered" count; each (customer, order_id) pair contributes its amount exactly once; and a customer with no qualifying orders does not appear in the result at all. The starting code is the version you inherited — it is wrong in three places.

SOLVE IT YOURSELF

Your turn — write it

Edit the stub, hit Run (or ⌘/Ctrl + Enter), and watch the hidden tests. Stuck? open the hints under the problem, or Reveal solution is one click away.

YOUR TASK

Read the inherited summarise(rows), find the three defects against the spec, and fix them one at a time.

CPython · WebAssembly
Examples and constraints 4 examples
EXAMPLE 1
Input summarise(R1), rows as customer/order_id/amount/status: acme/o1/100/shipped (twice), acme/o2/200/delivered
Output {'acme': 300}
the join fans out; the money does not, so o1 counts once
EXAMPLE 2
Input summarise(R2): acme/o1/100/shipped, acme/o3/999/pending, globex/o4/50/cancelled
Output {'acme': 100}
pending is excluded, and globex, whose only order was cancelled, is absent rather than 0
EXAMPLE 3
Input summarise(R3): acme/o9/10/shipped, globex/o9/20/shipped
Output {'acme': 10, 'globex': 20}
the same order reference under two customers counts for both
EXAMPLE 4
Input summarise([])
Output {}
edge case: no rows, no customers
CONSTRAINTS
  • Only "shipped" and "delivered" count. Every other status is excluded, including ones not seen in the fixtures.
  • Dedupe on the pair (customer, order_id) — the same order reference can legitimately appear under two customers.
  • A customer with no qualifying orders must be absent, not present with a total of 0.
  • Return a mapping of customer → total amount.
Hints 4 ideas
HINTS — 4 IDEAS
  1. Bug 1 is the status test: it excludes one bad status instead of including the two good ones.
  2. Bug 2 is the dedupe key: order_id alone silently drops a legitimate order belonging to a different customer.
  3. Bug 3 is the pre-seeding loop: it puts every customer in the result before any filtering, so cancelled-only customers survive with 0.
  4. Fix and re-run after each change. Knowing which fix moved which test is the point of the exercise.
Approach, complexity & discussion — open after you solve

Start from brute force

The obvious approach patches each failing test where it fails: add "pending" to the exclude-list, dedupe on the whole row instead of order_id, and strip zero totals at the end. Each patch is one line, the function stays O(n) time and O(n) space, and it passes every hidden test on this page. It is still wrong, because each patch encodes a symptom rather than the spec. A "returned" order still counts ({"acme": 30} instead of {}). Two legs of one order with different statuses count twice (200 instead of 100). And a qualifying order with amount 0 makes its customer vanish.

The approach

Check the inherited function against the spec one rule at a time; each defect maps to one line. Status: if r["status"] == "cancelled": continue is an exclude-list, so pending and any unknown status count. The fix is an include-list, if r["status"] not in ("shipped", "delivered"): continue. Dedupe key: key = r["order_id"] drops a second customer’s order that reuses the reference, so the key becomes (r["customer"], r["order_id"]), checked against seen before adding. Pre-seeding: the first loop, totals.setdefault(r["customer"], 0), puts every customer in the result before any filtering, so it goes, and totals[r["customer"]] = totals.get(r["customer"], 0) + r["amount"] creates a customer only when a qualifying row arrives.

The invariant: a customer is in totals exactly when at least one of their rows passed the status filter, and each surviving (customer, order_id) pair was added once. Fix one defect, rerun the tests, then the next.

Walk through an example

On these six rows the repaired function returns {"acme": 300, "globex": 40}, while the inherited code returns {"acme": 1299, "globex": 0, "initech": 0}, one wrong number per defect.

steprow (customer / order / amount / status)decisiontotals after
1acme / o1 / 100 / shippedcounts; key ("acme", "o1") is new{"acme": 100}
2acme / o1 / 100 / shippedkey already in seen: skip{"acme": 100}
3globex / o1 / 40 / deliveredsame order ref, other customer: key ("globex", "o1") is new{"acme": 100, "globex": 40}
4acme / o3 / 999 / pendingnot shipped or delivered: skip{"acme": 100, "globex": 40}
5initech / o5 / 70 / cancelledskip; initech never enters totals{"acme": 100, "globex": 40}
6acme / o2 / 200 / deliveredcounts; key is new{"acme": 300, "globex": 40}

Complexity

The repaired function is one pass: O(n) time for n rows, with O(1) average set and dict operations, and O(n) space in the worst case for seen, plus O(c) for totals over c customers. The inherited and symptom-patched versions are also O(n) time and space. Nothing here is about speed, only about which rows count.

Edge cases to test

  • An unknown status: a lone "returned" row gives {}. Every status outside the include-list is excluded, including ones no fixture shows.
  • Two legs of one order with different statuses, "shipped" and "delivered": the order counts once, {"acme": 100}, because the key is (customer, order_id), not the whole row.
  • A leg that fails the filter does not block one that passes: a "pending" leg is skipped before it reaches seen, so a later "shipped" leg of the same order still counts, giving {"acme": 100}.
  • A qualifying order with amount 0 keeps its customer, {"acme": 0}. Absent means no qualifying orders, not a zero total.
  • Status matching is exact: "Shipped" with a capital S is excluded. If upstream casing varies, normalize it on purpose rather than widening the list.
  • Duplicate legs with different amounts: the first row wins, 100 rather than 120. The spec says duplicates share an amount, so a mismatch is a data-quality signal worth logging.

Common mistakes

  • Rewriting from scratch instead of locating the defects — you lose the behavior that was already correct.
  • Deduping on order_id alone: two customers can legitimately carry the same order reference from different upstream systems.
  • Excluding only cancelled. The spec names the statuses that count; anything else — pending, returned, a typo — must not.
  • Leaving customers with a zero total in the output. Downstream, a zero looks like a real reported value.

Follow-ups an interviewer asks next

The rows no longer fit in memory. What changes?
Push the filter, the dedupe and the sum into the database: filter on status, group by customer and order_id to collapse the legs, then group by customer and sum the amounts. The database streams the rows, and a customer with no qualifying rows never forms a group, so absence comes for free.
How would you show the repaired totals are right before the customer sees them?
Reconcile against an independent source, such as the billing system or an invoice export, and list every customer whose total moved after the fix. Each changed number should trace back to one of the three defects. A change that traces to none of them is a fourth bug or a misread spec, and it should be explained before anything ships.
What if a duplicated order’s legs disagree on the amount?
The spec says they share the amount, so the reference keeps the first and moves on. In production a mismatch means the join is wrong or the data is bad, so count those cases and surface them. Picking the first, the largest or the sum changes reported revenue, which makes it a decision for the customer, not a default buried in code.
How do you stop the next engineer from reintroducing these bugs?
Turn each defect into a named test with the smallest fixture that shows it, as R2 and R3 do here, and write the spec’s three rules in the docstring. Make the include-list a named constant, such as COUNTED_STATUSES, so adding a status becomes a deliberate one-line change.

Where this shows up

This is the shape of a genuine FDE first week: an inherited pipeline whose numbers are subtly wrong, nobody who wrote it is still there, and the customer has been reporting from it for a year. The Palantir re-engineering round and Sierra’s planted-bug debugging round both test this directly. The skill is method — reproduce, narrow, fix one thing, verify — not speed.

Reference solution

Reference solution — Python
R1 = [                                    # provided fixtures
    {"customer": "acme", "order_id": "o1", "amount": 100, "status": "shipped"},
    {"customer": "acme", "order_id": "o1", "amount": 100, "status": "shipped"},
    {"customer": "acme", "order_id": "o2", "amount": 200, "status": "delivered"},
]
R2 = [
    {"customer": "acme", "order_id": "o1", "amount": 100, "status": "shipped"},
    {"customer": "acme", "order_id": "o3", "amount": 999, "status": "pending"},
    {"customer": "globex", "order_id": "o4", "amount": 50, "status": "cancelled"},
]
R3 = [                                        # same order ref, two customers
    {"customer": "acme", "order_id": "o9", "amount": 10, "status": "shipped"},
    {"customer": "globex", "order_id": "o9", "amount": 20, "status": "shipped"},
]


def summarise(rows):
    totals = {}
    seen = set()
    for r in rows:
        if r["status"] not in ("shipped", "delivered"):   # include-list, not exclude-list
            continue
        key = (r["customer"], r["order_id"])              # customer belongs in the key
        if key in seen:
            continue
        seen.add(key)
        totals[r["customer"]] = totals.get(r["customer"], 0) + r["amount"]
    return totals


print(summarise(R1))   # -> {'acme': 300}
print(summarise(R2))   # -> {'acme': 100}
Reference solution — TypeScript
const R1 = [                              // provided fixtures
  { customer: "acme", order_id: "o1", amount: 100, status: "shipped" },
  { customer: "acme", order_id: "o1", amount: 100, status: "shipped" },
  { customer: "acme", order_id: "o2", amount: 200, status: "delivered" },
];
const R2 = [
  { customer: "acme", order_id: "o1", amount: 100, status: "shipped" },
  { customer: "acme", order_id: "o3", amount: 999, status: "pending" },
  { customer: "globex", order_id: "o4", amount: 50, status: "cancelled" },
];
const R3 = [                                  // same order ref, two customers
  { customer: "acme", order_id: "o9", amount: 10, status: "shipped" },
  { customer: "globex", order_id: "o9", amount: 20, status: "shipped" },
];

function summarise(rows: any[]): Record<string, number> {
  const totals: Record<string, number> = {};
  const seen = new Set<string>();
  for (const r of rows) {
    if (r.status !== "shipped" && r.status !== "delivered") continue;   // include-list
    const key = r.customer + "|" + r.order_id;                          // customer in the key
    if (seen.has(key)) continue;
    seen.add(key);
    totals[r.customer] = (totals[r.customer] || 0) + r.amount;
  }
  return totals;
}

console.log(summarise(R1));   // -> { acme: 300 }
console.log(summarise(R2));   // -> { acme: 100 }

Questions about Re-Engineer a Legacy ETL

How do you debug inherited code you did not write?

Read it fully before changing anything, then check each line against the specification one rule at a time. Reproduce the wrong output with the smallest input that shows it, because a small fixture points at the responsible line. Fix one defect, rerun the tests, and confirm that only the expected test changed before touching the next one. A sweeping rewrite can pass the same tests, but it hides which change fixed what and throws away behavior that was already correct. In this challenge, that method finds three separate defects in a fourteen-line function.

Why use an include-list instead of an exclude-list for statuses?

An exclude-list counts everything it does not name, so every status that turns up later, such as pending, returned or a misspelled value, is counted by default. An include-list counts only what the specification names, so an unexpected status stays out until someone decides it should count. For revenue that is the safer failure: a missing order shows up as a lower total that someone can investigate, while an extra order silently inflates a number the customer reports upward. The inherited code counted a pending 999 order exactly this way.

Why must the dedupe key include the customer?

Because an order reference is only unique within one source. When data from several upstream systems is merged, two customers can carry the same order_id, as in fixture R3, where acme and globex both have o9. Deduplicating on order_id alone keeps the first row and silently drops the second customer’s legitimate order: a wrong number with no error. The key must be the combination that is actually unique, here the pair of customer and order_id. Choosing a dedupe key is a claim about the data, so check it against the source system.

Why should a customer with no qualifying orders be absent instead of showing 0?

Because downstream, 0 looks like a measured value. A report that lists globex with revenue 0 says globex bought nothing in the period, when the truth is that globex had no order that counts, which may mean no usable data at all. Leaving the customer out keeps those two cases apart, and a dashboard can still display a missing customer as zero if that is what its readers want. The inherited code created every customer up front with a total of 0, so a cancelled-only customer survived into the output.

Finished this one? 0 / 76 Challenges done
NextRepair the Spread Traversal Next on the “Interview escalations” ladder · rung 2 of 5

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.