Business Problem
The company's legacy CRM was a decade-old desktop-era product losing deals to modern cloud competitors. Agents lived on their phones between inspections and open homes, but the old system had no usable mobile experience, search that took 4-6 seconds over a few hundred thousand contacts, and no connection to the email and calendar tools agents actually used. Churn among sub-50-seat customers was accelerating quarter over quarter.
Leadership committed to a ground-up rebuild: a cloud CRM handling 12M contacts across 300 offices, fast search, live pipeline boards, and integrated communications. The non-negotiable constraint was migration: 300 offices of live business data, contact histories going back 15 years, had to move without loss and without any office losing more than one weekend of access. A botched migration would have been an extinction-level event for customer trust.
Architecture
I led the backend design: a modular Laravel application, deliberately not microservices at this stage, with hard module boundaries (contacts, pipeline, communications, reporting) enforced through internal contracts, giving us extraction options later without paying distributed-systems tax on day one. MySQL was the system of record with read replicas; Redis handled sessions, caching, and queues.
Search was its own subsystem: a denormalized search index maintained by queue workers consuming entity-change events, supporting prefix, fuzzy, and phone-number-shaped matching over 12M contacts at 120 ms p95. Contact timelines, every call, email, SMS, note, and inspection against a contact, used an append-only activity stream per contact, which made the highest-read surface in the product cheap to render and trivially cacheable.
Communications integrated SendGrid for tracked email and a telephony provider for click-to-call with recording, all writing back into the activity stream. The Vue.js SPA consumed a versioned REST API designed API-first, which let the mobile web experience and two early integration partners build against stable contracts while the UI was still evolving.
System Design
Modular monolith with contract-enforced module boundaries, chosen deliberately over microservices for a small team, with extraction seams documented for the future.
Denormalized, event-maintained search index delivering 120 ms p95 across 12M contacts, including phone-number and fuzzy-name matching tuned for real agent query patterns.
Append-only per-contact activity streams making timeline reads O(one range scan) and enabling aggressive caching of the hottest product surface.
Office-level data partitioning with row-level scoping, matching the real permission model of franchise groups sharing some data and isolating the rest.
Migration pipeline as a first-class product: repeatable per-office ETL with validation gates, dry runs, and automated rollback.
Read-replica routing for reporting so end-of-quarter analytics never touched the primary serving agent traffic.
My Responsibilities
Led backend architecture and a team of six engineers from first commit through GA and the full migration program.
Designed the data model, module boundaries, and the versioned API-first contracts consumed by web, mobile, and partners.
Built the search subsystem personally after the first prototype missed latency targets by an order of magnitude.
Owned the migration pipeline: per-office ETL, the validation gate suite, and the cutover runbook executed 300 times.
Ran capacity planning and load testing against modelled Monday-morning peak traffic (agents syncing after weekend opens).
Interviewed agents in the field quarterly, which repeatedly reshaped backend priorities, notably the phone-number search work.
Implementation
The first six months built the contact and pipeline core against a single pilot office running their real business on it, brutal but invaluable. Weekly sessions watching agents work exposed assumptions no spec review would have caught, including that phone-fragment search mattered more than any feature on our roadmap.
The migration program treated ETL as a product with its own roadmap. Legacy data required 40+ documented cleansing rules developed iteratively across dry runs. Office cutovers ran Friday night to Sunday, with the validation gate deciding go/no-go automatically and a support bridge on Monday morning. By office 50 the process was so routine we ran 12 per weekend.
Post-GA, performance work was continuous and data-driven: every endpoint carried latency budgets in monitoring, and the weekly performance review picked the worst offender. That cadence, more than any single optimization, is what kept p95s flat while contact volume grew 4x over two years.
Results
Migrated all 300 offices in 14 months with zero data-loss incidents and 97% of cutovers completing inside the weekend window.
Contact search p95 of 120 ms across 12M contacts, versus 4-6 seconds in the legacy system.
5,000+ daily active agents within a year of GA, with mobile web sessions representing 55% of usage.
Sub-50-seat customer churn reversed from accelerating to a 30% year-over-year reduction.
Platform absorbed 6x Monday-morning traffic spikes with p95 degradation held under 15%.
API-first design enabled two integration partners to launch alongside GA, an outcome that took the legacy product a decade not to achieve.
Lessons Learned
A modular monolith with enforced boundaries was the right call for the team size; we got the structure of services without the operational tax.
Migration is a product, not a task; the validation gate and dry-run discipline are what made 300 cutovers boring.
Watch real users work before optimizing anything; phone-fragment search was invisible in specs and decisive in adoption.
Scheduled, predictable load (Monday mornings) rewards scheduled, predictable scaling far more than reactive autoscaling.
Future Improvements
Extract the search subsystem to a dedicated service as the first proof of the documented module seams.
Native mobile apps with offline contact access for agents at inspections in poor-coverage areas.
Lead-scoring models over the activity stream data to prioritize agent follow-ups.
GraphQL gateway for integration partners who repeatedly request more flexible querying than the REST resources offer.
Challenges
Migrating 300 offices without losing trust
Each office had 15 years of quirky data in the legacy system. The migration pipeline ran a validation suite of 200+ checks (record counts, relationship integrity, financial totals, sampled deep comparisons) and any failure blocked cutover automatically. We ran every office as a dry run first. Across 300 production migrations we hit zero data-loss incidents and only three cutovers needed rollback and re-run.
Search that matches how agents think
Agents search by fragments: half a phone number, a misspelled surname, 'the buyer from the Smith St open'. Generic full-text search scored poorly on all of these. I built a purpose-tuned index with normalized phone n-grams, name phonetics, and recency weighting, validated against a corpus of real (anonymized) legacy search logs. Search success rate in usability tests went from 61% to 94%.
Monday-morning thundering herd
Traffic showed 6x spikes Monday 8-10 am as offices synced weekend activity. Rather than provisioning for peak, we combined queue-deferred non-critical writes, pre-warmed caches from predicted-active contact sets, and autoscaled read replicas on a schedule. Peak p95 held within 15% of off-peak.