← All notes
Technical Consulting7 min

Two sources of truth is one too many

Two maintained lists of the same projects are a bug with a delay on it. Our homepage collage lived in JSX while the projects page read WordPress — both drifted, including a live link to another client's domain. Consolidate to one document set; carry bespoke layout as opaque data if you must keep the design.

Nobody decides to have two sources of truth

It happens the way most architecture happens: one decision at a time, each locally reasonable. The homepage collage was hand-tuned, with bespoke sizing and position on every tile. Putting that in a CMS would have meant either a field for every CSS property or a design that lost what made the collage work. So it stayed as JSX. Fine.

The projects page came later, needed to be editable, and went into the CMS that already existed. Also fine. At that moment there were two lists of the same projects, maintained by different mechanisms, with no process connecting them.

The drift was not immediate. It never is. It took new work being added to one list and not the other, a client changing their domain, and enough months that nobody remembered the homepage was not reading from the CMS at all.

What we actually found

Two projects appeared on the homepage and were missing from the projects page entirely. Someone had added tiles to the collage — the visible, high-traffic surface — and not to the list. Entirely reasonable behavior, given the homepage is the page you actually look at.

One project's link pointed at a different client's domain. Not a dead link, not a typo in a path — a live website belonging to somebody else. That is the kind of bug invisible to every automated check you would normally run. The link resolved, returned a 200, and rendered a real site.

The rest was small and cumulative. Descriptions improved in one place and not the other. Capitalization that differed. Ordering that implied two different opinions about what our best work was. Nothing individually embarrassing, but collectively it was evidence that we did not have a list of our projects. We had two drafts of one.

What the drift actually looked like
SymptomRoot cause
Two projects on the homepage, absent from the indexTwo lists maintained by hand
A project link pointing at another client's domainNo check that a URL belonged to its project
An entry in the CMS the homepage never showedNeither list was a superset of the other

Nobody chose this. It's what two hand-maintained lists become, given enough months.

Why the usual defenses miss this

Link checkers pass, because the links work. Type checking passes, because both shapes are valid. Tests pass, because there were no tests asserting that the homepage and the projects page describe the same set of things — and why would there be? The two pages were built by different people at different times and had no reason to know about each other.

The only check that catches it is one that compares the two sources, and you only write that check after accepting that you have two sources. Which is the same as accepting that the design is wrong. Most teams would rather fix the design than instrument the duplication, and they are right.

The deeper point is that duplicated data is a different problem from duplicated code. Duplicated code gets flagged, refactored, argued about in review. Duplicated data just sits there being quietly inconsistent, and consistency is not something a compiler can help you with.

Consolidating without losing the collage

The obvious fix — move everything into the CMS — had a catch. The homepage collage was hand-tuned CSS, and a naive migration into a generic project list would have produced a grid rather than the thing that had actually been designed. Losing the design in order to fix the data would have been a bad trade.

So we carried the layout as data. Each project document gained the legacy CSS class names that position its tile, stored as opaque strings the CMS holds and the component applies. The collage kept its bespoke geometry. The content stopped being duplicated. Not elegant, but the alternatives were a worse homepage or a much longer project, and this was neither.

The projects page and the homepage now read the same documents. Adding a project is one action. The two lists cannot disagree, because there is one list.

The flag that keeps retired work out without deleting it

One wrinkle. Not everything belongs on the public list: older work, projects whose clients have moved on, sites we no longer want to point at. Deleting those documents would lose the record, and it would also change the totals we quote about our own history.

So there is a hidden flag. Hidden documents stay in the CMS, stay out of the public list, and still count toward totals. One boolean, and the question of whether something should be visible stops being answered by whether the document exists at all.

That is the general shape of the fix. One source, with fields that express the distinctions you used to express by choosing which copy to edit. If you find yourself maintaining two lists of the same thing, the second list is not redundancy. It is a bug with a delay on it.

How we look for it now

The question we added to scoping is boring and effective: where else does this list already exist? Not where will it live, but where does the client already maintain something that overlaps. A spreadsheet the sales team keeps. A PDF capabilities deck. A LinkedIn page. Those are all sources of truth in practice, and the ones nobody names during scoping are the ones that drift later.

In the codebase the check is a build-time assertion rather than a test: every project referenced anywhere on the site has to resolve to exactly one document. If a link, a tile, or a case study points at something that is not in the list, the build fails. It is a few lines, and it makes the invariant structural instead of remembered.

The counterpoint worth stating: sometimes two sources are correct and the answer is to accept the duplication and manage it. A marketing site and a product database will legitimately hold overlapping copy with different needs. What is not acceptable is duplication nobody decided on. The difference between a cache and a second source of truth is whether somebody chose it and wrote down which one wins.

Questions

How do duplicated content sources usually appear?
One reasonable decision at a time: a hand-tuned surface stays in code; an editable list goes into the CMS. Nothing connects them. Months later, new work lands on the visible page and not the other list — or links and copy diverge quietly.
Why don't link checkers and tests catch this?
Broken-link tools pass when URLs resolve. Types and page tests pass when each surface is locally valid. Only a comparison between sources catches drift, and teams rarely write that until they admit the design is wrong.
How do you retire work without deleting history?
A hidden flag on the single document set: stay in the CMS, leave public lists, still count in totals. Visibility becomes a field, not a second list you forget to edit.

Sources

  1. Sanity — content lake / single source patterns
  2. Next.js — data fetching overview
  3. Fathom NotesSame practice applied to consolidating our own site content.

Have something to build?

Tell us what you're working on and we'll tell you honestly whether we're the right fit.

Work with us