Infrastructure facts don't belong in service code

A fact about infrastructure stored in service code is a second source of truth, and two copies drift. Generate the view from the source instead.

Share
Infrastructure facts don't belong in service code
One definition is the source of truth for what should run — region-a, region-b, and a newly added region-c. It forks two ways. A copy kept by hand in service code never receives region-c and drifts apart. A view generated from the definition rebuilds with region-c included and stays aligned.

Two copies of one fact drift.

Keeping two copies in sync is an obligation, and every obligation that rests on a human remembering is a failure waiting for a busy week. The copies start identical. One changes, the other doesn't, and now the system holds two answers to the same question.

A view stored as state becomes a second copy

A fact about infrastructure that lives in service code is that the service doesn't own it.

A monitoring component needs to know what should be running in each region. The deploy system already holds a definition of what should run, and uses it to build the infrastructure. Pull the expected picture from the service's own code instead, and the service now keeps a second list of the same facts. Two copies of one fact. A new component in that definition does nothing to the expected picture, because the picture comes from the service code. The new component stays invisible to the monitor until someone updates that code by hand.

The expected picture is a view of the infrastructure. It's the same facts the definition already holds, shaped for one consumer. Stored in service code, that view takes on its own life, and a view with its own life is just a copy. The service that reads the fact ended up owning it.

Every infrastructure change becomes a code change

When the description of infrastructure lives in service code, that code becomes a gate on infrastructure. Adding a region, a component, or a newly declared address should never touch service code, yet each one now waits on the same code change a feature needs, with the review and deploy that implies. Work that should be a one-line edit to the definition takes on the full ceremony of a software release.

The friction is the visible cost. The drift is the expensive one. The definition changes because it deploys force. The expected picture lags, because updating it is a separate step, and separate steps get skipped under load. So the monitor checks reality against a map that no longer matches it. It goes quiet on components that were added because its map never learned about them. It alarms on components that have moved because its map still expects them to be where they were. The map and the live infrastructure drift apart in whichever direction is cheaper to leave alone.

Generate the view from the source

The fix removes the copy no one owns. Make the layer that owns the fact the only place it's defined, and have every consumer derive its view from there.

Generate the expected picture from that definition. Add a component, and the next run rebuilds the file to include it. No service code change, no code review, no software deploy. The file still exists, but every run rebuilds it from the definition, so it can't hold an answer the definition doesn't. (This holds only if the consumer regenerates at runtime. Bake the file into a build, and you've kept the copy, just moved it.)

The definition is the source of truth for what *should* run. The live infrastructure is still separate, and the monitor still checks the generated expectation against it. That check sharpens, because the expected side now comes from the definition, where it used to come from a stale hand-kept map. What vanished was one of two hand-maintained copies of the desired state. The desired-versus-actual comparison stayed.

The hard part is matching the old format. The generated file has to line up, field for field, with what consumers already parse. A projection still needs maintenance. The generator has one contract: read the definition, emit the format consumers expect. That contract changes far less often than infrastructure does.

The same trap shows up far from enterprise scale. Run a few services from a Git repository and deploy them automatically. The repository is the one place the desired state is defined, and the deploy converges the running services toward it. Keep a separate list of what should be running, and you've rebuilt a second copy. The precondition for drift is small: two copies of one fact, and enough time for one to change.

A copy without an owner is drift

A derived copy is sometimes worth keeping. A cache is a deliberate second copy: you trade staleness for speed or cost. A cache has an owner, a mechanism that bounds how stale it can get, whether by refreshing when the source changes or by expiring on a staleness bound someone chose. It can't diverge far, because someone built the mechanism that caps it.

The file in service code had none of that. No refresh trigger, no staleness bound, no owner: nothing kept it current but someone's memory. The test is ownership. Does the copy have someone who controls how far it can diverge? A copy with that owner is a cache. A copy without one is drift that hasn't surfaced yet.

Generation has a cost of its own. It spends work every time the view is derived. A stored copy pays per change. When the source rarely changes, and the view is read constantly, a stored copy with a refresh mechanism can be the right call. But that's a decision to own a cache, made on purpose, with the invalidation built. The old file was never that. It became a source of truth because no one set out to make it one.

The test runs against any system on your desk. Pick something the system stores and ask whether changing its source would require you to remember to update this copy too. If a human has to remember, it's a second copy.

Then check whether the source can rebuild it exactly. Sometimes it can't: the thing holds history, a human override, or a field the source never tracked. A thing the source can't rebuild is a record. When you can't tell whether you're holding a view or a record, treat it as a record. Give records an owner, and generate views from the source.

If it is a view, generate it from the source by default. Keep a stored copy only when the source rarely changes, the view is read constantly, and you build the refresh that bounds its drift. A schema redeclared inside the service that consumes it, a constant pasted into two configs: same question, same answer.

The copies you keep aligned by remembering are the ones already drifting. You just haven't deployed the change that reveals it.