Building Software · Case study
I Rebuilt a CRM Three Times. Here's What I Learned.
The first version of this CRM was built around what I assumed people needed. Much of what changed over three builds was how I approached the problem, not just the code.
I rebuilt an agency's CRM three times, working as an independent freelancer hired by the agency. The bigger problem turned out to be moving their existing contacts and deals out of Microsoft Access. This is a retrospective, written from memory. I am leaving out the agency's name, its data and the technical stack, because none of it is needed to explain what went wrong and some of it is not mine to share.
I designed it for myself
The first design came from a developer's assumptions, mine, rather than from how the people using the CRM actually behaved. I was thinking about what the system needed, not about what the people using it were trying to get done.
The people using it were not technical. Form validation was not strong enough, and some interactions were confusing for them. When a screen needs explaining, the screen is the bug.
The migration was the harder problem
The agency's existing contacts and deals lived in Microsoft Access, and they had to come across. The obvious approach is to import the contacts and import the deals. That is not enough, for a reason that is easy to miss until you hit it.
The new system generated its own IDs, so the legacy IDs were not simply carried over. Contacts and deals were related, and those relationships had to survive the move. Without a reliable mapping between each legacy record and the new ID it received, there is no dependable way to keep a deal connected to the right contact. Importing records independently can leave you with valid-looking records and the relationships between them lost.
So the critical piece was that mapping: for every source record, which destination ID it became.
Records that do not identify themselves
That mapping would be trivial if every contact were cleanly identifiable. They were not. The data had duplicate names, missing email addresses and inconsistent phone numbers. No single field could be trusted on its own to say that two records were the same person.
To find possible matches I used two rules: name plus email, and when there was no email, name plus phone. The important word is possible. Neither combination is guaranteed to be unique across the whole dataset, so a match under these rules is a candidate, not proof of identity.
The rules gave a shortlist, not a verdict. Treating a candidate as a confirmed match risks linking a deal to the wrong contact.
Letting the person who knows the data decide
Where the rules could not settle it, the import had a review step. It displayed the affected records so the responsible person could inspect them before the import was confirmed.
That keeps a decision the rules cannot settle with a person instead of leaving it to code. The trade-off is that a review step takes someone's time.
The legacy data also contained inconsistent dates and other messy values, which needed cleaning. I am not claiming I caught every problem the old data contained.
Three builds, one change in thinking
Looking back across the three builds, the shift was from implementation-first thinking to problem-first thinking. Instead of starting with what to build, start with who will use it, what the existing data really contains and what would be unacceptable to get wrong, such as a deal attached to the wrong contact.
These are the habits I take from it:
- Look at the real data early.
- Design around how the people using the system actually behave.
- Decide how record identity will work before writing an importer.
- Keep a person in the loop for matches the rules cannot settle.
I do not have metrics to share and I am not going to invent any. I also would not call the result perfect. It is an account of how the way I approached the work changed.
I wrote up the migration approach as a case study: CRM rebuild: making legacy data migration reliable. For a different project where the first architecture had to change, see how I rebuilt an Excel-like web app.