Business Problem
Marketing teams at client agencies spent money across Google Ads, listing portals, social campaigns, and phone tracking numbers, but lead attribution lived in four disconnected tools and a monthly spreadsheet exercise. Producing a single campaign ROI report took an analyst around 45 minutes per agency, and by the time it existed the spend decisions it should have informed were already made.
The product opportunity was a live attribution dashboard inside the platform agencies already used. The engineering problem was that lead events arrived from wildly heterogeneous sources, webhook bursts from portals, call-tracking posts, form submissions, and Google Ads API pulls, at around 500k events per day across tenants, and the dashboards had to aggregate them per campaign, per source, per agent, in under a second.
Architecture
I built an ingestion tier where every source adapter normalized events into a common lead-event schema and dropped them onto Redis-backed queues. Workers deduplicated, resolved identity (matching a phone call to an earlier web enquiry from the same person), applied attribution rules, and wrote to MySQL. The write model was append-only events; the read model was a set of pre-aggregated rollup tables maintained incrementally by the same workers.
The rollup design was the key decision. Instead of computing aggregates at query time, workers incremented per-tenant, per-campaign, per-day counters as events arrived. Dashboard queries became primary-key lookups over small rollup tables, which is how a shared MySQL instance served sub-second dashboards without exotic infrastructure. A nightly job recomputed rollups from the event log to correct any drift, keeping the fast path honest.
The Vue.js dashboard polled a lightweight summary endpoint every 30 seconds for the live view and hit the rollup API for drill-downs. Attribution rules, first-touch, last-touch, and a configurable weighted model, were applied at ingestion with results stored per event, so switching a report between models was a column choice rather than a recomputation.
System Design
Source-adapter pattern with a normalized event schema, making each new lead source an isolated adapter rather than a pipeline change.
Append-only event log as truth with incrementally maintained rollup tables as the read model, a pragmatic CQRS on plain MySQL.
Identity resolution window matching calls, forms, and portal enquiries into a single lead journey using phone, email, and click-ID signals.
All attribution models computed at ingestion and stored, trading cheap storage for instant model switching in reports.
Nightly rollup reconciliation from the event log, bounding any incremental-update drift to 24 hours.
Burst absorption via queue buffering sized for portal webhook storms of 50x average rate.
My Responsibilities
Designed and built the full pipeline: source adapters, normalization, identity resolution, attribution engine, and rollup maintenance.
Built the Google Ads and call-tracking integrations, including OAuth flows and quota-aware API polling.
Developed the Vue.js dashboard with the product designer, iterating directly with three pilot agencies.
Defined the attribution rule engine with marketing domain input, and documented model behavior for customer-facing teams.
Load-tested ingestion against recorded portal burst traffic and tuned queue and worker sizing.
Operated the pipeline in production and built the drift-reconciliation tooling after the first counter-skew incident.
Implementation
The first adapter, web forms, went end to end in two weeks to validate the schema and rollup approach with a pilot agency before generalizing. Each subsequent source followed the adapter contract: authenticate, fetch or receive, normalize, emit. The Google Ads adapter was the hardest, mostly due to API quota management across dozens of connected customer accounts.
Identity resolution shipped deliberately late, after two months of collecting raw events, because designing matching rules against real data beat designing them against assumptions. The observed patterns (portal enquiries followed by calls within 30 minutes dominated) directly shaped the matching windows.
The dashboard went through weekly iteration with pilot agencies. The most-used feature ended up being one we almost did not build: a per-agent response-time leaderboard, which agencies used to drive a measurable drop in lead response times and which became a selling point in demos.
Results
Campaign ROI reporting went from a 45-minute manual analyst exercise to a 3-second dashboard load.
92% of leads automatically attributed to a source and campaign across web, phone, and portal channels.
Pipeline sustained 500k events per day with p99 ingestion-to-dashboard latency under 2 minutes even during portal bursts.
Pilot agencies cut median lead response time from 42 minutes to 11 minutes using the response leaderboard.
Feature became part of the platform's top subscription tier and was cited in 14 competitive wins during the following year.
Zero additional infrastructure beyond existing MySQL and Redis; the rollup design kept incremental cost near zero.
Lessons Learned
Pre-aggregated read models on boring infrastructure outperform clever query-time solutions for dashboard workloads at this scale.
Collect real event data before designing identity resolution; the actual patterns will surprise you and simplify the rules.
A visible data-freshness indicator buys more user trust than invisible engineering heroics chasing perfect consistency.
The feature users love most often falls out of the data you already have; leave room for cheap experiments.
Future Improvements
Migrate the event log to a columnar store to enable ad-hoc historical analysis beyond the pre-built rollups.
Add multi-touch attribution with configurable decay curves now that per-event model storage has proven out.
Stream dashboard updates over WebSockets instead of 30-second polling.
Anomaly detection alerts on campaign metrics, flagging spend spikes and conversion drops without waiting for a human to look.
Challenges
Cross-channel identity resolution
A phone call 20 minutes after a web enquiry is usually the same person, but naive matching created false merges that corrupted funnels. I implemented tiered matching, exact identifiers first, then time-windowed fuzzy signals with a confidence floor, and made merges reversible with an audit trail. False-merge complaints stopped while attribution coverage held at 92%.
Portal webhook storms
Listing portals batched their webhook deliveries, producing bursts of 30-50k events in a few minutes against a 6-event-per-second average. Queue buffering with backpressure-aware workers absorbed bursts without dashboard staleness exceeding a couple of minutes, and without provisioning for peak permanently.
Rollup drift
Incrementally maintained counters inevitably drifted after worker crashes mid-batch. Rather than pursuing perfect exactly-once semantics on 2018-era infrastructure, I made the nightly full recomputation authoritative and surfaced a data-freshness indicator in the UI. Honest and simple beat theoretically pure.