Invariants in AI-Generated Code: What LLMs Can't Infer
The most insidious bugs in AI-generated code aren’t syntax errors or logic mistakes. They’re violations of invariants that were never written down—constraints that exist in the developer’s mind, in the codebase’s history, in the organization’s implicit knowledge, but nowhere the AI could access.
An invariant is a property that must always be true. “Account balances never go negative.” “A user session is always associated with a valid user.” “Timestamps are monotonically increasing.” When invariants hold, systems behave predictably. When they’re violated, systems fail in ways that are hard to diagnose and expensive to fix.
AI can’t infer invariants you haven’t specified. This is the fundamental gap in AI-assisted development, and closing it is the key to production-ready AI-generated code.
The Unwritten Invariant Problem
Consider a prompt: “Generate a function to transfer money between accounts.”
The AI produces something like:
def transfer(from_account, to_account, amount):
from_account.balance -= amount
to_account.balance += amount
This code is correct according to the prompt. It’s also dangerously incomplete:
- What if
amountis negative? The transfer becomes a theft. - What if
from_account.balance < amount? The balance goes negative. - What if the second operation fails? The money disappears.
- What if
from_account == to_account? The balance is corrupted.
These aren’t edge cases—they’re invariants. The real specification includes:
amount > 0(precondition)from_account.balance >= amount(precondition)from_account != to_account(precondition)sum(all_balances)is unchanged (invariant)- Either both operations succeed or neither does (atomicity invariant)
The AI didn’t know these because you didn’t say them. And you didn’t say them because they felt obvious. This gap—between what’s obvious to humans and what’s specified to AI—is where production failures hide.
Dijkstra’s Insight: Invariants as the Real Specification
Edsger Dijkstra, in “A Discipline of Programming” (1976), argued that the core of correct programs is not the code but the invariants the code maintains. The code is merely a mechanism for transitioning between states while preserving what must remain true.
This insight transforms how we think about AI code generation:
Traditional view: The code is the specification. Tests verify the code.
Dijkstra’s view: The invariants are the specification. Code implements the invariants. Tests verify that invariants hold.
Applied to AI: Specify the invariants. Let AI generate code. Verify that the code maintains the invariants.
When invariants are correct and the code maintains them, many implementations become acceptable—including AI-generated ones. When invariants are unstated, even “correct” code can be wrong.
Design by Contract for AI-Assisted Development
Bertrand Meyer’s Design by Contract (1988) provides the vocabulary for making invariants explicit:
Preconditions: What must be true before a function runs. These are the function’s requirements of its callers.
Postconditions: What will be true after a function runs. These are the function’s promises to its callers.
Class/Module Invariants: What must be true whenever an object is in a stable state (between method calls).
Applied to the transfer example:
def transfer(from_account, to_account, amount):
"""
Transfer money between accounts.
Preconditions:
- amount > 0
- from_account.balance >= amount
- from_account != to_account
- both accounts are active
Postconditions:
- from_account.balance == old(from_account.balance) - amount
- to_account.balance == old(to_account.balance) + amount
- sum of all balances unchanged
Invariants:
- no balance is ever negative
- all operations are atomic (full success or full rollback)
"""
# Implementation here
With this specification, AI can generate implementation and you have a contract to verify against.
Encoding Invariants at Multiple Levels
Invariants should be enforced mechanically, not just documented. Defense in depth:
Level 1: Type System
Make illegal states unrepresentable.
from typing import NewType
from dataclasses import dataclass
# Amount can't be negative by construction
PositiveAmount = NewType('PositiveAmount', Decimal)
def make_positive_amount(value: Decimal) -> PositiveAmount | None:
if value <= 0:
return None
return PositiveAmount(value)
@dataclass(frozen=True)
class Transfer:
from_account: AccountId
to_account: AccountId
amount: PositiveAmount
def __post_init__(self):
if self.from_account == self.to_account:
raise ValueError("Cannot transfer to same account")
The type system encodes constraints. AI-generated code using these types inherits the constraints automatically.
Level 2: Database Constraints
Enforce invariants at the persistence layer.
CREATE TABLE accounts (
id UUID PRIMARY KEY,
balance DECIMAL NOT NULL,
CONSTRAINT positive_balance CHECK (balance >= 0)
);
CREATE TABLE transfers (
id UUID PRIMARY KEY,
from_account UUID REFERENCES accounts(id),
to_account UUID REFERENCES accounts(id),
amount DECIMAL NOT NULL,
CONSTRAINT positive_amount CHECK (amount > 0),
CONSTRAINT different_accounts CHECK (from_account != to_account)
);
Even if AI-generated application code violates invariants, the database rejects the transaction.
Level 3: Runtime Assertions
Check invariants at system boundaries.
def transfer(from_account: Account, to_account: Account, amount: Decimal):
# Precondition checks
assert amount > 0, f"Amount must be positive, got {amount}"
assert from_account.balance >= amount, "Insufficient funds"
assert from_account.id != to_account.id, "Cannot transfer to same account"
initial_sum = from_account.balance + to_account.balance
# Implementation
from_account.balance -= amount
to_account.balance += amount
# Invariant check
assert from_account.balance >= 0, "Balance went negative"
assert from_account.balance + to_account.balance == initial_sum, "Money created or destroyed"
Assertions catch violations at runtime. In production, they trigger alerts. In development, they fail fast.
Level 4: Property-Based Tests
Verify invariants hold for generated inputs.
from hypothesis import given, strategies as st
@given(
balance1=st.decimals(min_value=0, max_value=10000),
balance2=st.decimals(min_value=0, max_value=10000),
amount=st.decimals(min_value=Decimal('0.01'), max_value=10000)
)
def test_transfer_preserves_total(balance1, balance2, amount):
if amount > balance1:
return # Skip invalid inputs
acc1 = Account(balance=balance1)
acc2 = Account(balance=balance2)
initial_total = acc1.balance + acc2.balance
transfer(acc1, acc2, amount)
assert acc1.balance + acc2.balance == initial_total
assert acc1.balance >= 0
assert acc2.balance >= 0
Property-based tests check invariants across a wide range of inputs, including edge cases AI might generate but humans wouldn’t test.
Common Invariant Categories
Consistency Invariants
- Foreign key relationships are valid
- Aggregate values match their components (sum of line items == order total)
- Cross-field constraints hold (end_date > start_date)
Safety Invariants
- Security-sensitive values are never logged
- Authentication state is always valid
- Permissions are checked before actions
Ordering Invariants
- Events are processed in causal order
- Versions are monotonically increasing
- State machine transitions are valid
Resource Invariants
- Allocated resources are eventually released
- Connection pools don’t leak
- File handles are closed
Each category represents implicit knowledge that AI can’t access unless you make it explicit.
Making Invariants AI-Accessible
For AI to generate invariant-respecting code, invariants must be in context:
Approach 1: Invariant Docstrings
Include invariants in the modules AI will reference:
"""
Account Module
INVARIANTS:
- Account.balance >= 0 always
- Sum of all account balances equals initial system balance
- All balance-changing operations are atomic
- Account.status must be ACTIVE for any balance operations
PRECONDITIONS for balance operations:
- User must be authenticated
- User must have permission for the account
- Amount must be positive
"""
Approach 2: Contract Files
Maintain explicit contract specifications AI can reference:
# contracts/accounts.yaml
module: accounts
invariants:
- name: positive_balance
description: Account balance is never negative
expression: "account.balance >= 0"
enforcement:
- database_constraint
- runtime_assertion
- name: balance_conservation
description: Total system balance is constant
expression: "sum(all_balances) == INITIAL_SYSTEM_BALANCE"
enforcement:
- audit_log_verification
Approach 3: Typed Interfaces
Use types that encode constraints:
class AccountService(Protocol):
def transfer(
self,
from_account: ActiveAccount, # Type enforces account is active
to_account: ActiveAccount,
amount: PositiveDecimal, # Type enforces positive
) -> TransferResult:
"""
Returns: TransferResult with either Success or InsufficientFunds
Never raises. Never returns partial success.
"""
...
The types themselves communicate invariants that AI can understand and respect.
When to Seek Expert Help
Identifying and encoding invariants is skilled work. Organizations benefit from external expertise when:
- Implicit knowledge is everywhere: Long-lived codebases accumulate invariants that no one remembers documenting
- AI-generated code is failing in production: Invariant violations often manifest as mysterious bugs
- Building new AI-assisted workflows: Getting invariants right upfront is easier than fixing them later
- Preparing for compliance audits: Regulators want to see how constraints are enforced
I help engineering teams identify hidden invariants, encode them mechanically, and build AI-assisted workflows that respect them.
Related Reading
- The Complete Guide to Production-Ready AI Development - The Vibes Inside Guardrails framework
- AI Agent Safety: The Substrate Pattern - Execution envelopes for agents
- Scaling AI-Assisted Development - Team patterns
Dipankar Sarkar is a technology advisor specializing in AI-native development and production systems. He has built ML systems serving hundreds of millions of users and helps organizations make implicit knowledge explicit for AI-assisted development. Learn more →