# invariant

> A row-level rule spanning several members, checked when the row is written. Use it when a constraint on one member is not enough — a balance that may not go negative, or a rule that only applies to some rows.

<!-- id: entity-invariants · area: entity · stability: stable · html: https://osysharp.com/reference/entity/invariants/ -->

## Summary        {#summary}
An `invariant` is a rule about **the whole row**, enforced when the row is written. A
[constraint](https://osysharp.com/reference/entity/constraints/) speaks about one member (`[Max(5)] int Priority`); an invariant speaks about the
relationship between members — and can be conditional, so it applies only to the rows it should.

## Signature      {#signature}
```osy syntax
entity <Name> {
  invariant <condition>;                                       // always holds
  invariant <condition> when <guard>;                          // holds only for rows where <guard> is true
  invariant <condition> when <guard> message "<explanation>";  // …and say why, when it fails
}
```

## Description    {#description}

### How do I write a rule over the whole row?   {#always}
The condition is written over the row's own members, and must be true when the row is written:

```osy title="a balance that may never go negative" test app=entity-invariants
entity Account {
  [Required] string Holder;
  decimal Balance;
  invariant Balance >= 0;
}
```

Any code path that would leave `Balance` negative — a withdrawal, an import, an adjustment — fails at commit. You do
not have to remember to check it, and neither does the next person.

### A rule that applies to some rows   {#when}
`when` guards the invariant. The rule is only checked for rows where the guard is true, so one entity can hold
several kinds of row without the rules of one contaminating the others:

```osy title="a rule that only applies to perishable goods" test app=entity-invariants
entity Product {
  [Required] string Name;
  bool IsPerishable;
  int ShelfDays;

  invariant ShelfDays <= 30 when IsPerishable
    message "Perishable items can have at most 30 shelf days.";
}
```

A tin of beans with `ShelfDays = 400` is fine — the guard is false, so the rule never fires. Milk with the same value
is rejected, and the person who tried gets the sentence you wrote rather than a constraint name.

### How do I say why it failed? — `message`   {#message}
`message` is the explanation a human sees. Without it they get a rule that says `ShelfDays <= 30`, which tells them
what was violated but not what to do. Write the message as though you are talking to the person who hit it — because
you are.

### What an invariant can see   {#scope}
The row it is on. An invariant reads the members of its own entity; it does not run a query and it does not reach
across into other rows. That is what keeps it cheap enough to check on every write.

## See also       {#see-also}
- [constraints](https://osysharp.com/reference/entity/constraints/) — the single-member rules (`[Required]`, `[Unique]`, `[Min]`/`[Max]`, `[Pattern]`)
- [entity members](https://osysharp.com/reference/entity/properties/) — the members an invariant reads
- [entity](https://osysharp.com/reference/entity/declaration/) — the entity an invariant guards
