The feature I was building was almost boring. My sports SaaS has a little “link your other sites” widget: if you run more than one site with us, you can cross-link them so teams and rosters show up in both places. I was extending it for a new context where an org admin can drill into a child event, and the widget started offering to link two kinds of sites it had no business listing. So I went to add a guard. The guard needed a flag. The flag did not exist. And the reason it did not exist turned out to be the actual story.

The widget got too friendly

The widget loops over the other accounts you own and renders a row for each one you can link. In the new event-folder world it started surfacing two kinds of accounts that are nonsense to link:

  1. Event-container accounts themselves, where the teams don’t even live (they live in pool accounts, not the event account).
  2. Sites that expired a long time ago.

The first one was a one-liner. Inside the loop, if the account is an event container, skip it:

if (accountInfo.isEventContainer) continue;

Done. I reached for the symmetrical version for the expired case, fully expecting it to be just as quick:

if (accountInfo.isExpired) continue;

And it did nothing. Every expired site still showed up in the list.

The flag that was never there

accountInfo.isExpired wasn’t false. It was undefined. The property had never been written anywhere the widget could reach it. Years of code, an account-expiry concept that drives billing and access, and there was no boolean a feature could branch on that said “this account is expired.”

What there was, was this: a helper called computeStatus() that ran on every account’s info object and set a single flag, expiringSoon, when a site was within a month of expiring. That’s it. The whole expiry-state machine, the part the rest of the app could see, was one bit that meant “expiring soon.” There was never a bit that meant “already expired.”

Here is the thing that stopped me. This was not a bug. For all those years, every consumer of computeStatus() only ever needed “expiring soon.” The dashboard wanted to show a yellow warning banner before renewal. The reminder email wanted to fire in the thirty-day window. Nobody, in all that time, ever needed to ask “is this site currently dead” at the point where they had the accountInfo object in hand. So the function computed exactly the half of the state its callers used and not one bit more.

The expired sites obviously existed. The database knew their expiry date. The system enforced their access. But the in-memory flag that a feature could branch on? That only covered the soon-to-expire case, because the soon-to-expire case is all anyone had ever asked for.

The fix was in the source, not the consumer

The wrong instinct here is to patch the widget. I could have grabbed the expiry timestamp directly in the widget loop, compared it to now, and moved on:

if (accountInfo.expiryTimestamp < now) continue; // don't do this

That works for exactly one call site and silently teaches the next person that “is it expired” is something each consumer recomputes by hand. It also means the answer can drift: if two parts of the app each open-code the comparison, they can disagree about where the cutoff is, what “now” means, whether you compare against midnight.

So the fix went back into computeStatus(), where the half-answer already lived, and made it compute the whole answer:

if (accountInfo.expiryTimestamp < now) {
accountInfo.isExpired = 1;
} else if (accountInfo.expiryTimestamp < monthFromNow) {
accountInfo.expiringSoon = 1;
}

One branch added. Now isExpired exists for everyone, computed in the status helper that owns expiry state and from the same underlying expiry timestamp as the nearby “expiring soon” flag. The widget guard became the trivial if (accountInfo.isExpired) continue; I expected in the first place, and any future feature that needs to know whether a site is dead can ask the object instead of redoing arithmetic.

This is not scope creep

When a new guard needs a flag that obviously should exist but doesn’t, the cheap move is to call it scope creep and route around it. You came here to fix a widget, not to audit the expiry model, so you grab the raw timestamp, ship the widget, and leave the gap.

I want to argue the opposite. The missing flag is not a distraction from the feature. It is the feature doing its job. A new consumer is the first thing in years to ask the data model a question the data model had never been asked, and it got back a blank stare. That gap is information. The new feature fuzz-tested the existing model and found a real hole: a state the system fully enforces but never names.

Old code computes the state it personally needs and stops. That’s not laziness, it’s good discipline, you don’t build out a state machine on spec for callers that don’t exist. But it means the model is shaped like the union of its current consumers, not like the domain. The domain has “expired accounts.” The model only had “accounts expiring soon,” because that’s the only slice anyone had stood in front of and needed.

Add a genuinely new consumer with a genuinely different question and it can expose a gap that existing flows never exercised. In this case, the guard did nothing because the boolean it tested had never been assigned. When a missing flag represents a real shared domain fact, do not route around it with a one-off comparison; let the gap guide an improvement to the shared model.