AI Engineering

Invariants in AI-Generated Code: What LLMs Can't Infer

· 8 min read

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 amount is 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.

Get in touch →


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 →