The problem with invariants is that they change over time

kiyanwang | 117 points

I would recommend not calling such things invariants and not thinking about them in the same way you think about invariants. If you would allow a comparison to physics then you would call such things assumptions: frictionless pulleys, small angle deflections, much slower than light speed, less than nuclear density. The physical theory developed is then correct while the assumptions hold. If you are making such an assumption and your language supports it, add a debug assertion if possible.

For me, invariants are constructs of the design of an algorithm. A list doesn't change while iterating over it-by construction, not because someone else owns the reference and promises not to change it concurrently. This structure can only be instantiated via this function which ensures that that string field can always contains only numeric digits. Those are invariants, thing you enforce by design so you can rely on that later. Assumptions instead are things you do not enforce but rely on anyways.

Back to physics, an invariant would generally be something like conservation of energy or the speed of light being universal. Both of these things are only invariant in certain physical theories which enforce them by construction.

BlackFly | 10 days ago

Looking back, I find it hard to offer concrete advice. So many of the "things I know to be true" changed over time, while so many of the "doing x to allow for future y" didn't lead to anything.

On the other hand, I have systems being worked on and still shipping 20 years old because of fortuitous decisions in the 90s. Most of those taken because of bad experiences with earlier systems.

For example, I'm a big fan of surrogate keys and UUIDs, in database design. Mostly because of experience where they weren't used which caused problems later. (By contrast, performance costs of using them become lower every year.)

Conversely deciding to build code libraries parallel to application development means long-term gains in performance and reliability.

It's easy to view all this in hindsight, -much- harder with foresight.

Today I try and see everything through 2 rules.

1) beware of "always" statements, especially especially in regard to client requirements. IME "always" is used as a synonym for "mostly" and "at the moment".

2) choose paths now that keep options open later. Will this system ever use distributed data? Probably not, but choose a primary key that leaves that option open.

Lastly, and this applies to data design, remember "nothing is universal". Using Social Security number as an ID is great, until they outsource. Phone number is always 8 digits? Yeah right. Nobody works night shift... you get the idea.

If in doubt, err on the side of "variant". You'll be right more often than not.

bruce511 | 10 days ago

The problem I think is that when you define an invariant, you are also defining an infinite number of "implicit" invariants. And it's hard to know a priori which of those implicit invariants is not an actual invariant over time.

Take a simple User type

  type User {
    name: string
    email: string
  }
This describes some obvious invariants - that a User will have a name and an email.

But it's also describing some less obvious invariants, e.g. that a User will not have a phone number. This has implications if a phone number ever does get added to a User type, and your system has to deal with two different versions of Users.

(This isn't just an academic problem - the problem of trying to evolve fields over time is the reason why Protobufs got rid of the idea of a "required" field.)

And this is an example of a very simple implicit invariant. In practice, systems that interact with each other will give rise to emergent behaviour that one might think is always true, but is not guaranteed.

yen223 | 10 days ago

I'm happy to see people talking about the problem posed by implicit invariants, but this post strikes me as defeatist. This line especially:

> Implicit invariants are, by definition, impossible to enforce explicitly.

Yes, Hyrum's Law (and, at the limit, Rice's Theorem) and all that means that we won't ever be perfect, but just because an invariant is currently implicit doesn't mean it must remain implicit forever. We can identify classes of implicit invariants and make them explicit using things like type systems.

kibwen | 11 days ago

Isn't the whole point of the term "invariant" that it describes something as unchanging under specific circumstances.

e.g.

The sum of the angles of triangles is 180 degrees in the context of euclidean geometry. However, if we project a triangle on a sphere, this no longer holds. So the sum of the angles is an invariant under euclidean geometry.

On the other hand, the value of PI is a constant because it stays the same regardless of the circumstances. That's why all the numbers themselves are constant as well - the number 5 is number 5 absolutely always.

So if you have a value that changes over time, it is definitely not a constant. It could be invariant, if you, e.g. specify that the value does not change as long as time does not change. Your value is now an invariant in the context of stopped time, but it can never be a constant if there is any context where it does change.

Swiffy0 | 10 days ago

Usually an invariant is something you explicitly state about a data structure or algorithm, and which must hold before and after an operation, but can be temporarily violated.

What you are talking about here are assumptions, which are usually implicit and sometimes not even part of the thought process. One purpose of writing a design document is considering and stating the relevant assumptions.

larsrc | 10 days ago

It's still way easier to handle it when you know where it will break(even it is far way from you) instead of just let it break everywhere. Make it just explode and fix it is still much better than a unknown bug that you never know if it is fixed haunting you forever.

Rust for example. Enforce the invariant everywhere. It's probably harder to build, but it is also easier to find out where and why it won't instead of have infinite memory safety problems everywhere.

And about implicit invariant? Probably a simple code change can fix it, a test would cover it, or an assertion. Or you need a whole new language to describe and test it safely(like rust) because the one you are using just can't. People are not even close to find out the answer of this problem, but the direction is to make it explicit.

mmis1000 | 10 days ago

Yes, it's called evolution. Software evolves as well as scales. Scaling is architected growth, evolution is unarchitected growth. (Sometimes scaling results in unarchitected growth, if the architecture was not reasonable.) There are many patterns for handling evolution, but they almost always involve a pattern outside of the existing architecture (a super architecture) to lean on for support. In my opinion, codifying and optimizing these super architecture patterns is one of the highest goals in software engineering because they allow for less error prone evolutions.

throwaway74432 | 10 days ago

I have always used asserts to enforce invariants (sometimes not just actual "assert" but things like `Preconditions` in Java which are always enabled). Those will actually break during testing if they ever change.

I understand you cannot assert all invariants, but as far as I can see, that's your main tool against this problem.

brabel | 10 days ago

IMHO, an "invariant" is only relative to the current system to be build, based on a specific set of specifications. As soon as you build a new version of the system (either because you change the functionalities, the implementation or because the environment change), you're expected to check which invariant still applies and which should be added, removed or changed...

In a way, "invariant" are the same as "tests sets": they are here to help to ensure that the product will have some specific properties. Both are used during the product development and checked. And both must be updated when the specs change

olivierduval | 10 days ago

Specifications can change over time. It sounds to me as though the developer who made the change to the system that invalidated the invariant of the original specification wasn't aware of the spec in the first place. Maybe they should have worked on the specification first before proceeding.

Sounds like a communication issue to me.

agentultra | 10 days ago

The way invariants are used can be different than “this will always be true.”

It’s a problem solving tool. I sometimes use the term to mean “I will assume this to be true in order to solve an intractable problem. If that succeeds, I need to find a way to guarantee this is actually invariant.”

shermantanktop | 10 days ago

When I read this story it leaves me wondering if these people had any automated testing. This may or may not have helped but it is not even mentioned and it does sound like a problem that an integration test might have caught.

cjfd | 10 days ago

> That assumption became obsolete the moment that Matt implemented task packing, but we didn’t notice. This code, which was still simple and easy to read, was now also wrong.

It seems like Matt did not fully understand the platform he was developing for and introduced a bug as his code did not satisfy the invariants.

It is really nice when invariants are checked either in types or in tests so Matt would have been alerted when he introduced the bug.

I don't like the discourse the article introduces. It must always be the one who writes the newest code who has the responsibility to adjust previous code. That also includes changing obselete modules (where invariants does no suffice anymore).

tossandthrow | 10 days ago

implicit or explicit assumptions, or invariants that can actually vary, should be minimized. Otherwise, try to explicit them with guards and exceptions. This should never happen, so I'll do that - when you think this, stop and put an "if(this) then Error" in your code. Breaking is better than not breaking, lurking, and costing money later. My opinion.

fedeb95 | 10 days ago

Sure sometimes there are deprecations and API changes. Assumptions must be revised accordingly.

sixthDot | 10 days ago

Osborn’s Law: Variables won’t; constants aren’t.

stevep98 | 9 days ago