Why your CRM shows three of the same customer
Repeated customer records usually come from a schema that stores one person per customer, not from weak matching. When a household contains several people with different phone numbers and emails, every channel creates its own record, and tightening the matcher makes it worse because aggressive matching is what merges the wrong people permanently. The fix is a contacts layer beneath the customer record, plus an ordered resolution rule that says which record wins.
Three records for one customer is usually not a matching failure. It is a schema that assumes one person per customer, meeting a household that contains three. The husband calls from his mobile, the wife texts from hers, the son submits the web form with the shared email, and every one of those identifiers is genuinely different. Your automation is not confused. It is doing exactly what you told it to do, against a record structure that has nowhere to put a second person.
That distinction decides the fix. If it is a matching problem, you tune the matcher. If it is a structural problem, tuning the matcher makes things worse.
The duplicates are honest
Open the three records and look at what is actually in them. Different phone numbers. Possibly different emails. The same address, if anyone captured it. No field contains the same value twice, which is why no deduplication rule based on exact matching will ever join them.
This is the moment most teams reach for fuzzy matching, and it is the wrong instinct. To catch a household you would have to match on surname plus street, or on some similarity score, and that class of rule does not fail gracefully. It fails by attaching one person's conversation to a different person's record.
We inherited a system that matched phone numbers by substring containment, which sounds reasonable until you notice that a seven digit local number is contained inside plenty of longer strings that belong to other people. It had been quietly producing false positives. The failure looks nothing like a duplicate. It looks like a customer receiving someone else's appointment reminder.
Why deduplication makes it worse
The two errors are not symmetrical, and that asymmetry should drive every decision here.
A missed match costs you a manual merge. Somebody notices two records, joins them, and the day continues. An incorrect match costs you a customer relationship, and in most CRMs it is permanent. HubSpot's documentation is blunt about it: it is not possible to unmerge records. Their suggested recovery is to strip an email off the survivor and build a new record by hand, which is not a recovery so much as an admission. There is even a ceiling, since records that have been through a combined 250 merges cannot be merged again at all.
So the sensible posture is conservative matching plus somewhere to put the ambiguity, which is the argument laid out in more detail in matching customer records without a shared ID. That piece is about joining two systems. This one is about a single system whose shape is wrong, and the two fixes stack.
The fix is a layer, not a better matcher
Put a contacts layer underneath the customer record. Every phone number and email belongs to a contact. Every contact belongs to exactly one customer. A message from any of them lands on the shared record.
The three rows collapse into one customer with three contacts, and none of the identifiers had to be guessed at, merged, or thrown away. Nothing was made fuzzy. The structure simply grew a level that reality already had.
The knock-on effects are where the value actually shows up. Conversation history stops splitting, because the thread is keyed on the customer rather than on the phone number that happened to send the message. Outbound sends can resolve the number that last spoke with that customer, so a reply arrives in the existing thread on the customer's phone instead of opening a new one from an unfamiliar number, which reads as spam. On a system we run for a New England window and door replacement contractor with two brands across two states, the contacts layer had to be rolled out across six separate intake and call workflows before any of that held, which is the honest cost of this change: the structure is simple, the rollout is not.
Write the resolution contract down
The part teams skip is agreeing what an incoming identifier resolves to, in what order, everywhere. Skip it and you get workflows that each resolve the same phone number slightly differently, which produces duplicates again through a new door.
Ours is four rules, and the specific choices matter less than the fact that they are written down and applied identically:
Search the customer records first, then the contacts beneath them. A contact match resolves up to its parent customer, so the answer is always a customer record regardless of which level matched. When the same identifier matches at both levels, the customer record wins, because a tie means someone entered the same number in two places and the higher level is the one every workflow already writes to. An identifier that is unusable, blank, malformed, or a placeholder, matches nothing at all rather than matching everything, which is the single rule most likely to be missing from a hand-rolled matcher.
On normalization, be deliberate. We compare US phone numbers on the last ten digits after stripping formatting, which replaced an older approach that stored five literal format variants and matched on string equality across them. Last-ten-digits is a pragmatic shortcut, not a standard. The standard is E.164, which caps a number at 15 digits with an explicit country code, and it is the right answer the moment a single international number enters your data. Email comparison is lowercased, since case is not meaningful in the mailbox part for any provider you will realistically encounter.
Whichever you choose, record the choice and what will break it. The assumption you never wrote down is the one that fails on a Tuesday.
What the household model does not fix
Three honest limits, because a structure that quietly does less than you think is worse than one you understand.
Suppression granularity is the important one. If the do-not-contact flag lives on the customer record, honoring it silences the whole household. That is safe, and it is blunt: one person opting out stops messages to a spouse who never asked for that. Per-person suppression needs the flag on the contact and every outbound workflow reading it there. We currently enforce at the household level, and saying so is the point.
Link population is the second. A message table can carry references to the customer, the contact, and the account, and having the fields is not the same as having the handlers fill them in. Ours are only partly populated today. Check what your writes actually store rather than what your schema permits.
The third is that a contacts layer does not tell you which person is the decision maker. That is a business fact, not a structural one, and it belongs in your qualification step rather than in your data model. Where that matters most is intake and lead capture, which is where the wrong answer costs a wasted appointment.
What to do next
Pull your five most recent customer records and check whether any of them share an address, a surname, or a domain. If they do, count how many separate follow-up sequences those records are currently running against what is really one buying decision. That number is the cost you are paying, and it is usually larger than anyone expects.
Then decide the structural question before touching the matcher: does your customer record have room for a second person. If the answer is no, no amount of matching logic will save you, and the data model work is the actual project. If you want a second pair of eyes on the resolution rules before you roll them across every workflow that touches identity, send us the ones you have.
Frequently Asked Questions
SOURCES & CITATIONS
- Merge records — HubSpothttps://knowledge.hubspot.com/records/merge-records
- What is E.164? — Twilio Docshttps://www.twilio.com/docs/glossary/what-e164
About Alexey Yushkin
Alexey is the founder of GENERAL INFORMATICS LLC. He designs and ships AI and automation systems for businesses and operators across the US.
Related reading
Want this kind of system in your business?
We build practical AI and automation systems for operators. Send us your current workflow and we will show you what to automate first.
