The Bug That Reports Success
Here is a failure with no error message anywhere. A reader adds a second feed from a publisher it already carries — the world section, when it already had the top-stories section. The interface confirms the source was added. The shelf binding appears. Fetch rounds run clean. And the new shelf fills with the old feed's articles, forever, because the two rows were treated as the same source.
The cause is that the code deduplicated on the name. And the name, in this system, was not typed by a human at all — the client derives a label for a syndication source from its hostname, so two different paths on one host produce the same label. Get-or-create matched the label, found the existing row, returned it, and reported success. Nothing was wrong from any code path's point of view.
Identity Belongs to the Thing That Determines Behavior
The general principle is worth stating plainly, because it generalizes far past feeds: an entity's identity is whatever determines what it does. For a source, what it does is entirely decided by its kind and its configuration — which endpoint, which query, which locale. The name determines nothing; it is a label for humans, and labels are allowed to collide.
Deduplicating on a label rather than on the behavior-determining fields produces exactly this class of bug: a silent aliasing where the system believes it has two things and has one. Note that the reverse mistake — never deduplicating at all — is not harmless either. Identical configurations genuinely are one feed, and giving each shelf its own row means fetching the same endpoint several times per round, which is precisely the impoliteness the previous lesson was about.
Compare Parsed, Not Serialized
One implementation detail carries real weight. Configurations are usually stored as serialized JSON, and it is tempting to compare the stored strings. Do not. Two configurations differing only in key order — or in whitespace, or in unicode escaping — are the same configuration, but their serializations differ. Parse both sides and compare the resulting structures, so that identity is a property of the configuration rather than of how it happened to be written.
Then De-Collide the Label
Once identity moves to the configuration, the name is free to be merely a display string — and it must be allowed to collide gracefully. A new row whose label is taken gets a suffix. That is all. The label being ugly is a much smaller problem than the label being load-bearing.