The first time I built this CRM, I thought I understood the problem.
I knew how to build forms, connect database tables, create features, and import data. I was a developer. I knew how software was supposed to work.
But I had overlooked something important: knowing how to build software isn't the same as understanding the problem you're solving.
I ended up rebuilding the CRM three times before I got the solution right.
And the biggest lesson had very little to do with writing code.
I built it for developers, not users
The CRM was built for an agency whose employees weren't necessarily technical.
My first mistake was designing the application around what I thought people would naturally understand.
If a field was labelled Email, I expected users to enter an email address. If there was a button to expand a menu, I expected them to click it.
So I didn't put enough thought into validation and interaction design.
That assumption didn't hold up in practice.
Users don't always follow the workflow a developer imagines. They make mistakes, misunderstand interfaces, and enter data in unexpected ways.
A form shouldn't rely on someone having common sense about what belongs in each field. It should help them do the right thing and prevent avoidable mistakes.
That was my first lesson: software needs to account for how people actually behave, not how we expect them to behave.
But the user interface wasn't the biggest problem.
The real problem was hidden in the old data
The agency already had its data in Microsoft Access. The goal was to move that data into the new CRM.
I built an import feature for contacts and deals.
At first glance, that sounds straightforward. Read the records from the old system, insert them into the new database, and you're done.
Except contacts and deals weren't independent records. They were related.
A deal belonged to a contact. When importing the data, the new CRM generated its own IDs. That was intentional: I didn't want to blindly reuse the identifiers from the legacy system.
But I hadn't designed a reliable mapping between the old records and the new ones.
Imagine importing 5,000 contacts and then importing 2,000 deals. How would the CRM know which newly created contact belonged to each deal?
The original IDs weren't being preserved, and the old data didn't always provide a reliable alternative.
I had created an import feature without fully accounting for the relationships that made the data meaningful.
Importing records was easy. Preserving their relationships was the real challenge.
Names and emails weren't enough
The old system had another complication.
Some deals contained a person's name and email, but no reliable contact ID. That meant the importer had to figure out which contact a deal belonged to.
What if two contacts had the same name?
What if an email was missing?
What if only a mobile phone number was available?
Matching by name alone could connect a deal to the wrong person. Matching by email wasn't always possible. And assuming every record was clean would have made things worse.
I needed a better way to identify candidate matches without pretending the data was more reliable than it really was.
The solution: matching rules and human verification
I changed the approach.
Instead of treating every record as an isolated row to import, I introduced matching rules to help identify possible relationships.
For example, I could compare a person's name and email. When an email wasn't available, I could use the name and phone number as an alternative.
These combinations helped narrow down the possibilities, but they weren't a guarantee. Two records could still be ambiguous, and I didn't want the software making a potentially incorrect decision silently.
So I added a review step.
Before confirming the import, the system displayed the affected records in a pop-up so the responsible person could review them. Even when there were thousands of records, the process needed to make potential issues visible rather than blindly accepting questionable matches.
The goal wasn't to pretend every record could be matched automatically.
The goal was to make uncertainty visible and give the responsible person a chance to resolve it.
I also had to clean up inconsistent date formats and other problems in the legacy data. Moving data between systems meant dealing with what actually existed in the old system, not what I wished had been there.
There was no need to introduce AI into this problem. It required careful reasoning, matching rules, data validation, and human verification.
Why did I rebuild it three times?
Because I started implementing before I had fully understood the problem.
Each time, I had to go back, reconsider the approach, and start again. It cost time, but each attempt exposed assumptions I hadn't challenged.
Looking back, I don't think the central lesson was that the CRM needed better code.
It needed a better understanding of the requirements.
I should have asked earlier:
- What makes a record identifiable when the source system has no reliable ID?
- How will relationships survive when destination IDs are generated from scratch?
- What happens when the source data is incomplete or inconsistent?
- Which decisions can the software make safely, and which require human review?
- How will we detect a questionable match before it becomes a data-integrity problem?
These questions would have changed the implementation from the beginning.
I now think about the problem before the technology
When I was more junior, I tended to jump straight into implementation. I'd think about the database, the forms, the features, and how to build them.
Now, I try to slow down first.
I look at the problem, the requirements, the constraints, and the limitations. I write down my assumptions and challenge them before turning them into code.
That doesn't mean I can anticipate every complication. Real projects will always surprise you.
But it does mean I can avoid building an entire solution on an assumption that should have been questioned at the start.
The CRM taught me that lesson through three rebuilds.
Good engineering isn't just about making software work. It's about understanding what needs to work, for whom, and under which constraints, before you start building.
About the author
I'm Ayman Atif, a software engineer focused on building business software with .NET, C#, Python, and web technologies. I've been programming since 2014, and my experience has taught me that solving the right problem matters more than writing code quickly.
🌐 Portfolio: https://ayman-atif.vercel.app/
Top comments (0)