Caity Neighbor
The collection
High (Cross-System)Specimen 05

Three Systems, One Contact Record

How Persistent State Broke Attribution

Prevents cross-system data contamination by enforcing interaction-level state instead of shared contact data.

System snapshot

Enforced interaction-level state in a shared contact model, so each survey reflected only the interaction it belonged to.

The Survey With the Wrong Name on It

A customer walked into a store and was helped by Alex. They received a CSAT survey about their experience.

Weeks later, the same customer called the finance team and worked with Jordan. They received another CSAT survey.

Three months later, as part of their regular rotation, they received a brand-level NPS survey asking the classic question: “How likely are you to recommend [Company] to a friend or colleague?”

An NPS survey isn't about a single visit and is independent of transactional experiences over time, such as the one with Jordan or Alex.

The survey went out with Jordan’s name on it anyway.

When the customer scored as a detractor, Jordan’s NPS score felt the blame. Reporting was relying on a mix of true and false scores, and the people reading those reports understandably stopped trusting them.

That was the failure I was brought in on. A feedback program attributing scores to the wrong people and making individual NPS scores (and the platform’s value) meaningless.


How the Shared Record Works

The platform kept one record per customer, keyed by email.

Three upstream systems wrote to that record: retail interactions, financial workflows, and web events.

Each one triggered its own kind of survey on its own schedule:

  • recurring brand-level NPS, on a time-based cadence
  • transactional CSAT, fired by an event
  • web feedback, popped in-app

On the record, fields persisted unless something explicitly overwrote them (for example, Alex was overwritten by Jordan as the Customer Service Representative).

Each system, on its own, behaved exactly as designed, and that’s the whole problem.

Every value written to the record stayed there, with no confirmed link to the interaction that produced it, so the record couldn't tell a current value from a leftover one.

So when Jordan's name landed on the record during the finance call, it stayed there.

When the NPS scheduler fired three months later, it read the record as it stood and pulled Jordan's name into a brand-level survey.


Leftover Values Pile Up


The Complication

The data itself was never wrong. The trouble was that a value, once written, outlived the interaction it came from, so a leftover value and a current one were impossible to tell apart.

The shared contact model was core platform behavior, not a setting. Fields persisting until being explicitly overwritten was how the system stored data, full stop.

A few constraints narrowed the room further:

  • It was a cautious pilot. The implementation was live only lightly, with surveys sending at low volume, and this discovery made a sensitive champion even more wary.
  • I had no visibility into the source systems. An outside dev team maintained them, billing by the hour and already running over budget, so every change I needed had to be specified precisely and handed off. The rework carried real cost. I worked through a customer-side stakeholder who drove the internal coordination I couldn't reach.
  • The schema had to be locked early. The source systems adopted it in stages, each rollout carrying its own survey requirements, so the field definitions had to be right before the first one shipped. Revising them later meant more billable work and more pain for my champion.
  • The survey types interfered. Global suppression rules meant recurring NPS and transactional CSAT could step on each other's delivery, so any change to one had to account for the others.

The Solve

I couldn't change how the platform stored data, so I had to change something else: the data being sent from each source.

The shift was to make each interaction define the full record for that moment, rather than letting the record accumulate values over time.

In practice, every upstream system started sending a full, context-specific payload on each interaction. Each payload carried the complete picture at that moment, not just the fields it updated, and included null fields to clear anything that did not apply.

If a finance interaction had no representative, the payload included a null representative field, which was then cleared. The record stopped carrying yesterday's context into today's survey.

I locked one shared field schema up front, before any system built against it, so a complete payload meant the same thing everywhere.

The systems then adopted it in stages, each rollout carrying its own survey requirements, but all speaking the same definitions.

💡
The survey trigger changed, too.
  • Before, a scheduler fired surveys and pulled whatever happened to be on the record at that moment.
  • After, surveys fired by API at the moment of the interaction or at a defined milestone, like the three-month NPS mark, and each trigger carried its own fully defined payload.

The survey captured the customer's context as it was at that point, not as the record had drifted to.

I validated it one survey type at a time, starting with NPS and confirming clean, isolated attribution before moving on to multiple CSAT surveys and then web surveys (there were dozens with more to come).

Only once each produced a correct context on its own did I let it run across the whole system.


What the Fix Enforced

The fix came down to four rules:

  • One shared schema. Every system spoke the same field definitions, so a complete payload meant the same thing across retail, finance, and web.
  • A full payload on every interaction. Each message carried the entire picture for that moment, not only the fields it was changing.
  • Explicit clearing. Any field that did not apply was actively emptied, so nothing carried over by default.
  • Capture at the moment. Surveys fired by API at the interaction or milestone, against a record that described only that moment.

System Flow

system flow

Impact

Result 01

Survey responses reflected the interaction they were actually about.

Result 02

Team-level reporting matched what customers had experienced.

Result 03

Stakeholders who had stopped trusting the numbers started trusting them again.

Result 04

Cross-system contamination, where one system's data surfaced in another's survey, stopped.


What I'd Improve

Improvement 01

Native scoping

The cleanest version of this lives in the platform, with each value tied to the interaction that set it, so a value cannot outlive the one it came from. We worked around the absence of that, and building it in would remove the coordination cost.

Improvement 02

Schema templating

Onboarding a fourth or fifth source still means hand-aligning it to the shared schema. A template would turn each new system into a fill-in-the-blanks job instead of a fresh negotiation.

Improvement 03

Reporting flexibility

Some of the rigidity came from reporting that leaned on the record's current state. Loosening that would lighten how much each payload has to carry.

System Details

Signals
Data ModelingConstraint-Based LogicState Management
Built with
Multi-System Integration Environment · APIs · Contact-Based Survey Platform · CRM