Cross-channel matching: the bug that only appears at site number two

An EAN lookup without channelId works fine — until the same product shows up on a second channel. How an audit in the MFP platform found five such leaks and which patterns made the wrong path impossible afterwards.

Cross-channel matching: the bug that only appears at site number two

Two identical suitcases carrying the same luggage tag, each on a different baggage belt

One channel: everything works

A typical e-commerce codebase starts single-channel. One database, one product catalogue, one store. An EAN lookup is simply findByEan($ean): there is only one channel, so the query is unambiguous by definition. ProductRepository does its job, AiBulkService does its job, exports do their job. No ambiguity, no problem.

Two channels: everything seems to work

When a second channel lands — at MFP (Multi Feed Processing) that was first ditpetje.nl and dithorloge.nl, then ditparfum.nl — the nature of the problem changes silently. Products are now stored per channel, with the same EAN potentially active on multiple channels at once. findByEan($ean) still returns a product — just not always the product belonging to the channel the caller meant.

The failure mode is treacherous: no crash, no 500, no log line. Just a product detail page on ditpetje.nl showing, by accident, a watch from dithorloge.nl, or an AI enrichment job pairing caps copy with perfume data. The bug presents as bad content rather than a technical error, and slips past the usual alarm signals.

The audit and what it found

A targeted audit on the MFP platform traced every call to findBy(['id' => $ids]) and findBy(['ean' => $ean]) and looked for instances where the channelId criterion was missing. Five cross-channel leaks surfaced in:

  • ProductService — bulk lookups that joined products across channel boundaries.
  • AiBulkService — AI-enrichment batches into which a wrong channel could slip.
  • OutputQueueService — exports that merged products from multiple channels into one feed.
  • GuideController — editorial guides fetching products without a channel filter.
  • ProductEnrichCommand — a CLI command operating across unintended channels.

The fix: make the wrong path impossible

The direct fix is to add channelId everywhere. The durable fix is different. At MFP the rule now is that all ProductRepository queries must carry a channelId criterion. No findBy(['ean' => $ean]) without a channel. No findBy(['id' => $ids]) without a channel. The repository is shaped so that the channel-less call yields a PHPStan error, or in stricter variants a type error. The wrong path becomes impossible to write, not merely discouraged.

The lesson for multi-anything systems

Cross-channel matching is a multi-tenant bug wearing a different uniform. Every codebase that one day moves from one to two tenants, channels, organisations or customers carries this risk — and most codebases only discover it when a customer screenshots a strange product page. The preventive work is cheap: a lint rule, a PHPStan rule, a review checklist. The reactive work is expensive: clearing data corruption, restoring customer trust, running retrospectives. Designing for N before there are 2 is the cheapest form of defensive engineering a multi-channel platform can afford.

Curious what is possible?

One conversation is usually enough to tell whether it fits. A reply within 24 hours, and no middlemen.

  • A reply within 24 hours, always personal
  • Half an hour on the phone gets you the order of magnitude
  • A first conversation without obligation
  • Including software originally built by someone else