
TL;DR
- Exact identifiers must always outrank fuzzy similarity scores in matching decisions
- Five unrelated bugs produced the same symptom: orders booked to wrong customers
- Similarity scores only suggest candidates, never book orders autonomously
- Dormant code paths can be woken by backfills, migrations, or new data
- Test matching fixes against real historical order data before deploying
If you're building customer matching automation into an order pipeline, here's the answer before the argument: similarity scores are for suggestions. Exact identifiers are for decisions. An account number, a customer ID, a confirmed mapping in your own system outranks any fuzzy score, every time, no matter how close the match looks. The moment a "pretty sure" match gets to book an order under a customer record, you've handed the system permission to guess with someone else's account. It will take you up on it.
We know this because we watched the same bug walk in through five different doors.
The same misroute, five costumes
An order gets booked to the wrong customer. Not a bad product, not a wrong quantity, the order itself lands under the wrong account. That's the failure mode. What's strange is how many unrelated-looking causes produce the identical symptom.
We saw it show up as a shared portal domain, where two separate customers submitted orders through what looked to the matcher like the same source. We saw it as a shared network domain, the kind of thing that happens when two companies sit behind the same IT infrastructure and every email header looks like it came from one shop. We saw it as an affiliate's email address, a legitimate partner submitting on behalf of a customer, but close enough in name and pattern that the matcher folded the two together. We saw a semantic embedding match do it: two customer names that meant almost the same thing to a model trained on similarity, and meant two completely different bank accounts to everyone else. And we saw a brand name embedded inside another company's name do it, where a substring match found a real brand token sitting inside an unrelated business's legal name and called that a hit.
Five root causes. One symptom. If you'd looked at any single incident in isolation, you'd have written five different fixes and shipped five different patches to five different code paths, each one specific to the thing you'd just seen. That's the trap. The symptom is identical because the underlying mistake is identical: something that resembles a customer got treated as if it confirmed one.
Why similarity feels safe (and isn't)
Fuzzy matching earns trust the same way every convenient shortcut does: it works, most of the time, right up until the moment it's confidently wrong. A similarity score of 0.94 reads like precision. It has a decimal point. It looks like the kind of number a careful system produces. But 0.94 similar is not 100% identical, and the entire risk of customer matching automation lives in that gap.
The uncomfortable part is that fuzzy matching isn't wrong as a technique. It's wrong as a decision-maker. Similarity is exactly the right tool for narrowing a list of candidates, for surfacing "here are three accounts this might be," for handing a human or a downstream exact check a short list instead of the entire customer table. It is the wrong tool for closing the loop by itself, because a similarity score has no idea what it doesn't know. It can't tell you "these two companies happen to share a mail server" versus "these two companies are the same company." It just knows they look close, and close is not a business decision, it's an invitation to check.
We've made this same argument about human review elsewhere, and it holds here in a slightly different shape: a check that can be talked into approving is not a check. A matcher that can be talked into matching by resemblance alone is not a matcher, it's a rumor generator with a confidence score attached.
The precedence rule
Here's the rule we now build to, in order:
First, an exact identifier wins outright. A customer ID, an account number, a confirmed API key, a portal login tied one-to-one to a known account. If one of these is present and it resolves cleanly, the order books to that customer and nothing else gets consulted. No similarity score, no matter how high, overrides an exact identifier. This isn't a tiebreaker rule, it's a precedence rule. Exact beats fuzzy before fuzzy is even allowed to speak.
Second, a confirmed mapping wins next. If there's no exact identifier on the order itself but there's a previously confirmed, human-verified mapping (this email address belongs to this account, this portal login has been tied to this customer before and a person signed off on it), that mapping can resolve the order. It's not a fresh guess. It's a decision that already happened once, correctly, and is now being reused.
Third, and only third, similarity gets a voice, and it only gets to suggest. If neither of the above resolves the order, a fuzzy match can propose a candidate customer. It goes to a person, or to a narrower deterministic check, not to a booking. The system says "this looks like it might be Customer X, confidence 0.89, here's why," and a human either confirms it or rejects it. The moment you let that 0.89 book on its own, you've quietly moved fuzzy matching from step three to step one, and you won't notice you did it until an order lands on the wrong account and somebody has to explain why.
Written as a sentence: exact identifier, then confirmed mapping, then suggestion only. Nothing close-enough books an order. If your matcher can't tell you which of those three tiers made a given decision, you don't have customer matching automation, you have customer matching improvisation, and it will feel fine for a long time before it doesn't.
The dormant path that a backfill woke up
One of the five disguises deserves its own paragraph, because it's the one that should worry you most if you're auditing your own pipeline right now.
The semantic embedding match wasn't misrouting anything for a while. Not because it was safe, but because the code path that used it wasn't actively firing. It sat there, dormant, doing no visible harm, because the conditions that triggered it hadn't come up yet.
Then a data backfill ran. Historical records got reprocessed, older orders got re-keyed into a system that hadn't held them before, and suddenly the embedding match had inputs it had never seen live: names and patterns from a different era of the business, different formatting conventions, different affiliate relationships. The dormant path lit up. It started firing matches on backfilled data using exactly the same logic that had been sitting quietly, and those matches were exactly as unreliable as they would have been if the path had been live the whole time. Nothing about the bug changed. What changed is that the data finally exercised it.
This is the part that a lot of teams miss when they think about risk in a matching system: the risk isn't only in what's currently running against live traffic. It's in every code path that exists in the system, whether or not it's currently getting exercised, because a backfill, a migration, a new data source, or a new customer segment can wake up a path that's been asleep since it was written. If you wrote the embedding matcher two years ago and it hasn't caused a visible problem since, that's not proof it's safe. It might just mean nobody's fed it the input that breaks it yet.
The operational lesson: audit dead code the same way you audit live code, especially in a matching system. Ask what data would have to show up for each path to fire, and ask whether a backfill, a new integration, or a new customer type could produce that data tomorrow. Don't wait for the backfill to teach you the hard way.
Replay as the judge, not your intuition
Here's the part that surprised us most, and the part we think matters most for anyone building this.
Every candidate fix we tried for the misroute fixed some cases and broke others. Tighten the exact-match requirement on portal domains, and you fix the shared-portal-domain misroute but you also start rejecting legitimate customers who happen to share hosting with someone else. Loosen the embedding threshold, and you catch fewer false positives on the semantic match but you also start missing real matches that were previously working. There was no version of "just fix it" that didn't trade one failure mode for another. That's not a failure of engineering effort. It's the shape of the problem. A matching system sits in a space where every rule you tighten closes one door and opens another, and you cannot know which doors moved by reading the code. You have to run it.
That's what replay is for. Before any fix went live, we replayed it against stored past orders and asked: does this rule produce the same result as before on the cases that were already correct, and does it flip the result on the cases that were wrong? A fix that flips the wrong cases and holds the right cases steady is a real fix. A fix that flips some wrong cases while also flipping previously-correct cases into new mistakes is not a fix, it's a trade, and you need to see the trade before you ship it, not after a customer calls.
This is also how we caught a fix that looked reasonable on paper and was actually harmful in practice. A first-cut gate, built to catch exactly this kind of misroute, got killed in replay because when we ran it against real order history, it flagged mostly good orders. It looked, on the whiteboard, like a sensible tightening. Run against the actual population of past orders, it turned out to be a tax on legitimate business dressed up as a safety check. Replay caught that before a single real customer felt it. Intuition about what "should" work wouldn't have.
If you're building customer matching automation and you don't have a replay harness, that's the gap to close before you touch the matching logic itself. Not after. Before. Every rule change is a hypothesis about which orders should route where, and the only honest way to test a hypothesis about historical behavior is against historical data, not against your mental model of what the rule is supposed to do.
What this means for the rule you write down
If you're the one responsible for a matching system that touches customer identity, write the precedence rule down explicitly, in the code and in the runbook, not just in your head:
- Exact identifier resolves the order. Full stop, no override.
- Confirmed, human-verified mapping resolves the order if no exact identifier is present.
- Similarity produces a suggestion, routed to a person or a narrower deterministic check. It never resolves an order on its own.
Then go find every dormant path in your matching logic and ask what would wake it up. A backfill, a new integration, a new customer segment, a data migration you're planning for next quarter. If you can't answer what would trigger a given path, that's not a theoretical gap, that's the exact shape of the incident we described above, just waiting for its own backfill.
And before you ship any change to the matching logic, run it through replay against real history first. Not a synthetic test set you built to prove your fix works. Real orders, the ones that were already correctly matched and the ones that were already wrong. A fix that can't show you both sides of that ledger isn't ready, no matter how confident it looks on paper.
We've written about the broader version of this problem, the general case of building checks and gates around AI-touched decisions, in Human-in-the-Loop Is Not a Safeguard and in Who Checks the Checker? Managing Agents Is a Measurement Problem. If you're deciding more broadly where automation should carry the decision and where it should only carry the suggestion, AI Assistance vs Automation: The Decision Tier That Matters lays out the same precedence logic in a wider frame. Customer matching is one instance of a rule that applies everywhere you're tempted to let a confidence score make a decision that used to require an exact match or a person.
If you're looking at your own order pipeline and you're not sure whether your matcher is deciding or suggesting, that's worth a real conversation, not a guess. Book a discovery call and we'll walk through what your matching logic is actually doing, tier by tier, before it costs you an order booked to the wrong account.