Skip to content
Bitloops - Git captures what changed. Bitloops captures why.
HomeAbout usDocsBlog
ResourcesAI Code Governance & QualityEncoding Business Rules and Domain Invariants: Making Domain Knowledge Machine-Readable

Encoding Business Rules and Domain Invariants: Making Domain Knowledge Machine-Readable

Your domain has rules: order totals stay positive, emails stay unique, money doesn't disappear. Encode these as machine-readable invariants and agents can't violate them—even if they've never heard of them before.

13 min readUpdated March 4, 2026AI Code Governance & Quality

The Silent Failure

Your AI agent just generated code to apply a discount to an order. The code compiles, passes tests, and goes to production. Six hours later, a customer has an order with a total of -$47. The discount was applied twice. Now your financial reporting is wrong, your database is inconsistent, and you're scrambling to figure out what happened.

The agent didn't violate any architectural constraint. The code didn't break any naming convention. It didn't introduce a circular dependency. It just violated a domain invariant: "order total must never be negative."

Domain invariants are business rules that must hold regardless of how code is written or generated. They're not optional; they're non-negotiable. An order total is never negative. A user's email must be unique. A transaction must have an audit trail. A payment can't be processed before the order is confirmed. These aren't programming guidelines—they're facts about how your business works.

Without making these invariants machine-readable and enforced, AI agents (and humans) will violate them. With invariants encoded as executable validators, you catch violations immediately.

Why AI Agents Violate Domain Invariants

An AI agent has no domain knowledge. It has training data, your code context, and your prompt. It understands programming constructs: classes, functions, loops, patterns. It doesn't understand business logic unless you encode it explicitly.

When you ask an agent to "add a discount feature," the agent sees code about applying discounts and generating updated totals. It doesn't inherently know that a total must be non-negative. It doesn't know that discounts can't exceed the original price. It doesn't know that certain user types get certain discount caps.

The agent might:

  • Apply the discount without checking if the result is negative (violates invariant)
  • Apply discounts multiple times if the code doesn't prevent it (violates invariant)
  • Let one user type apply discounts meant for another (violates business rule)
  • Create a discount that makes some orders free but others unprofitable (violates pricing invariants)

These aren't bugs in the agent's reasoning. The agent wasn't given the domain knowledge. You didn't tell it "the order total must never be negative." So it optimized for the immediate task: apply the discount. It didn't think about the invariants.

This is fundamentally different from architectural constraints. An architectural constraint is about structure ("presentation layer can't access database directly"). Domain invariants are about values ("this integer must never be negative").

Domain invariants are what turn AI-generated code from "might technically work" into "will actually maintain business correctness."

What Domain Invariants Look Like

A domain invariant is a rule that must hold in the state of your system. It's about values and relationships, not code structure.

Financial domains are full of invariants:

  • Transaction amounts must be non-negative
  • Account balances must be consistent with transaction history
  • Settlement must occur within X days
  • Refunds can't exceed the original transaction
  • Currency amounts must have the correct precision (2 decimals for most currencies)

E-commerce domains:

  • Order totals must match the sum of line items
  • Line item prices must match product catalog prices (at time of order)
  • Discounts can't exceed order total
  • Inventory quantities must be non-negative
  • Shipped orders can't be modified

Healthcare domains:

  • Patient records can't be accessed without authorization
  • Prescriptions must be within dosing guidelines
  • Patient consent must be documented for any use of data
  • Medical records must be immutable once finalized

User management domains:

  • Email addresses must be unique
  • Usernames must meet format requirements
  • Passwords must meet security requirements
  • User roles must be from a defined set
  • Admins can't be deleted without reassigning their permissions

Authentication domains:

  • Sessions can't exist after logout
  • Password reset tokens expire after X minutes
  • Login attempts are rate-limited
  • Multi-factor authentication codes are single-use

These are rules about the state of your system. They're not guidelines; they're laws. Break them and your system is in an invalid state.

Encoding Invariants as Validators

Invariants live in code as validators. A validator is a function that checks whether a state violates an invariant and either allows it, rejects it, or raises an exception.

The simplest invariants are property checks: "this value must be non-negative."

class Order {
  private _total: number;

  applyDiscount(amount: number): void {
    const newTotal = this._total - amount;
    if (newTotal < 0) {
      throw new DomainException(
        `Order total cannot be negative. Current: ${this._total}, Discount: ${amount}`
      );
    }
    this._total = newTotal;
  }
}
Python

This validator checks a single property. The invariant is enforced: you can't set an order total to negative. If code tries (generated or human-written), it fails immediately.

More complex invariants involve relationships between multiple entities.

class User {
  private _email: string;

  static async changeEmail(
    userId: string,
    newEmail: string,
    emailRepository: EmailRepository
  ): Promise<void> {
    // Invariant: email must be unique across all users
    const existingUser = await emailRepository.findByEmail(newEmail);
    if (existingUser && existingUser.id !== userId) {
      throw new DomainException(
        `Email ${newEmail} is already in use. Invariant: emails must be unique.`
      );
    }
    // Safe to change email now
  }
}
Python

This validator checks a relationship: the email must not already exist (except for the current user). It's a cross-entity invariant. Checking it requires access to other data (all users).

Domain invariants often require validation at multiple layers:

Schema level: The database enforces uniqueness on the email column. This is the last resort—the database refuses to insert duplicate emails.

Domain level: The domain model throws an exception if you try to create an invalid state. This catches violations before they hit the database.

Application level: Use cases validate input before creating domain objects. This catches invalid requests early.

API level: Request validation ensures input format is correct. This is the outermost layer.

Working backwards: API validation catches format errors. Application validation catches semantic errors. Domain validation catches business rule violations. Database constraints catch everything else. Layered validation means invariants are checked at multiple points; a violation at any layer is caught.

The Spectrum: From Simple to Complex

Domain invariants range from simple property checks to complex multi-entity validations.

Simple invariants: Single property, single rule. "Amount must be non-negative." Checked in the property setter or constructor. Fast, obvious, caught immediately.

Conditional invariants: "If status is 'shipped', then tracking_number must exist." Checked when status changes or when state is completed. Slightly more complex logic.

Cross-entity invariants: "If order is paid, the payment must have a corresponding transaction in the ledger." Checked when order or payment changes. Requires access to multiple entities.

Temporal invariants: "Refund must occur within 30 days of purchase." Checked at refund time, validated against purchase timestamp. Time-dependent.

Dependent invariants: "If user has role 'admin', then permissions must include 'delete_user'." Checked when role or permissions change. One invariant depends on another.

Aggregate invariants: "Sum of all line items in order must equal order total." Requires validating across multiple child entities. Aggregate consistency.

The pattern: simple invariants are checked in one place. Complex invariants might require checks across multiple layers and entities.

For AI code generation, simple invariants are your first priority. If you encode "amount must be non-negative," the agent can't generate code that violates it. As you grow, you add more complex invariants. Each one is another constraint on what code is valid.

Validation Techniques

Different invariants need different validation techniques.

Constructor validation: Validate when an object is created. Ensure the object can never exist in an invalid state.

class Order {
  private total: number;
  private items: OrderItem[];

  constructor(items: OrderItem[]) {
    // Invariant: order must have items
    if (items.length === 0) {
      throw new DomainException("Order must contain at least one item");
    }
    // Invariant: total must match items
    const calculatedTotal = items.reduce((sum, item) => sum + item.total, 0);
    if (calculatedTotal <= 0) {
      throw new DomainException("Order total must be positive");
    }
    this.items = items;
    this.total = calculatedTotal;
  }
}
Python

Constructor validation prevents invalid objects from ever existing.

Method-level validation: Validate when state changes.

transferFunds(from: Account, to: Account, amount: number): void {
  if (from.balance < amount) {
    throw new DomainException("Insufficient funds");
  }
  from.withdraw(amount);
  to.deposit(amount);
}
Python

Method validation ensures transitions maintain invariants.

Query validation: Validate when retrieving data. Ensure retrieved data satisfies invariants.

async getUser(id: string): Promise<User> {
  const user = await this.userRepository.findById(id);
  if (!user) {
    throw new UserNotFound();
  }
  // Invariant: all users must have an email
  if (!user.email) {
    throw new CorruptDataException("User without email found");
  }
  return user;
}
Python

Query validation catches data corruption.

Aggregate validation: Validate consistency across related entities.

validateOrderConsistency(order: Order): void {
  const itemsTotal = order.items.reduce((sum, item) =>
    sum + (item.quantity * item.pricePerUnit), 0
  );

  if (Math.abs(order.total - itemsTotal) > 0.01) {
    throw new DomainException(
      `Order total ${order.total} doesn't match items total ${itemsTotal}`
    );
  }
}
Python

Aggregate validation ensures complex consistency rules.

Property-based testing: Generate random valid and invalid inputs, verify invariants hold for valid inputs.

property('order total is never negative', () => {
  const discount = generateValidDiscount(); // 0 to order.total
  const order = generateOrder();
  const newOrder = order.applyDiscount(discount);
  assert(newOrder.total >= 0);
});

property('applying discount twice to the same order fails', () => {
  const order = generateOrder();
  order.applyDiscount(validAmount);
  expect(() => order.applyDiscount(validAmount)).toThrow();
});
javascript

Property-based testing finds edge cases in invariant logic that unit tests might miss.

Domain Invariants Across Different Industries

Invariants are specific to your domain, but the patterns repeat.

Fintech: Settlement rules are invariants. "Payment can't be debited until authorization is confirmed." "Refunds must match authorized amount within tolerance." "Daily withdrawal limits can't be exceeded." These are encoded in the payment processing engine. AI agents can't bypass them because they're enforced at the service level.

Healthcare: Data handling invariants. "Patient consent must be documented before any use of data." "Prescriptions must be within dosing guidelines for patient weight/age." "Medical records must be immutable after 30 days." These are enforced in the domain models and at database constraints.

E-commerce: Pricing and inventory invariants. "Discounts can't make order negative." "Inventory quantity must be non-negative." "Refunds must not exceed original sale price." Encoded in order and inventory services.

SaaS: Subscription invariants. "Customer can't have more than one active subscription." "Billing cycle can't be shorter than minimum." "Usage can't exceed plan limits without overage charges." Enforced in billing and subscription services.

The domain knowledge is different, but the pattern is the same: identify what must never be violated, encode it as a validator, and run it before state changes. The AI agent can't violate rules it can't break.

Making Invariants Discoverable

A validator in your code is only useful if people (and AI agents) know it exists and understand when to use it.

Documentation helps:

/**
 * Applies a discount to the order.
 *
 * Invariants enforced:
 * - Order total must never be negative
 * - Discount cannot exceed current order total
 * - Only one discount per order (this method throws if called twice)
 *
 * @throws DomainException if invariant would be violated
 */
applyDiscount(amount: number): void { ... }
Text

Naming helps. A method like applyDiscount() is generic. A method like applyDiscountWithinLimits() hints that there are limits being enforced.

Exceptions with context help:

throw new DomainException(
  `Cannot apply discount of $${amount} to order total $${this.total}. ` +
  `Invariant: order total must never be negative. ` +
  `Maximum allowed discount: $${this.total}.`
);
Text

The error message tells you: what you tried to do, why it failed, and what the rule is. An AI agent reading this exception message can learn the invariant and avoid violating it in the future.

Tests document invariants:

describe('Order invariants', () => {
  it('order total cannot be negative', () => {
    const order = new Order([...items]);
    expect(() => order.applyDiscount(order.total + 1)).toThrow();
  });

  it('discount cannot be applied twice', () => {
    const order = new Order([...items]);
    order.applyDiscount(50);
    expect(() => order.applyDiscount(50)).toThrow();
  });
});
javascript

Tests show what violates an invariant and confirm it's caught.

For AI agents, the clearer your documentation, the less they'll violate your invariants. If the code says "invariant: email must be unique" and throws an exception when that's violated, an agent regenerating code that violates that invariant will see the exception and try a different approach.

Invariants and AI Code Generation

When using AI agents to generate code, invariants become your control surface as part of broader pre-commit and CI validation pipelines.

An agent generates code → the code hits a domain validator → the validator rejects it → the agent (or human) sees why → regenerates → tries again.

This is powerful because it turns domain knowledge into executable rules. The agent doesn't need to understand why an order can't be negative. It just needs to know that violating the rule causes an exception, and keeps trying until the exception doesn't happen.

This is how you get AI code that respects your business logic even though the agent doesn't understand your business.

Tools like Bitloops encode invariants as part of the constraint system: validators aren't just enforcement mechanisms, they're part of the code generation context. The agent knows "when you modify this value, check the invariant," and violations surface with context about the domain rule, not just the code error. This makes AI-generated code not just syntactically correct but semantically correct—it respects your business rules. Over time, the compounding quality improvement loop means agents make fewer invariant violations as they learn from prior corrections.

The practical upside: your domain knowledge becomes a guard rail that AI agents can't escape. And the guard rail is machine-readable, testable, and executable.

FAQ

Isn't this just normal programming? Why is it special for AI?

It is normal programming. The difference: humans learn domain invariants through experience. A developer who's been in fintech for 5 years knows "transaction amounts must be non-negative" without being told. They don't need it as an explicit rule. An AI agent, generating code on first encounter with the domain, has no such intuition. Explicit invariants turn that knowledge into something the agent can't bypass.

What if we have so many invariants that validation becomes slow?

That's a sign you have too many invariants encoded inefficiently. Prioritize: which invariants do you care most about? Start with those, enforce them strictly. Secondary invariants can be warnings. And optimize: validate only when necessary (constructor, state change), not on every operation.

Can invariants conflict with each other?

Sometimes, which usually means one of them is wrong. If "order must have items" and "order can have zero items during checkout" conflict, one is miscoded or there are actually two different states that should be represented differently (draft order vs. confirmed order). Conflicting invariants are a sign of domain modeling issues.

How do we test that our invariants are being enforced?

Write tests that specifically violate each invariant and confirm they're rejected. "Try to create an order with zero items—expect exception." "Try to apply discount twice—expect exception." These are invariant-specific tests, not feature tests.

What if a valid business scenario requires temporarily violating an invariant?

Usually this means the invariant is too strict or the domain model is wrong. For example, if you need an order total to temporarily be negative, your order model is wrong—you should have line items and then a separate "total discount," not allow negative totals. Think through the domain model again.

How do we version invariants? If we change a rule, how do we handle existing data?

Invariants typically don't change—they're laws of your domain. If you need to change one, that's a migration problem. Data created under the old invariant might violate the new one. You decide: clean up the data, add an exception for historical data, or grandfather in old data. This is architectural, not code-level.

Can AI agents suggest new invariants?

Not really. Invariants are domain knowledge that comes from understanding your business. An agent can highlight unusual patterns ("I see code checking if amount is negative in 10 places; maybe that should be an invariant?"), but the decision is yours. Domain expertise can't be learned from tokens.

How do we balance strict invariant enforcement with development velocity?

Strict invariants actually improve velocity because they prevent bugs that would otherwise need to be debugged and fixed. The short-term cost of enforcing an invariant is outweighed by not having to track down bugs caused by invariant violations. And in an AI-generation context, invariants prevent entire categories of hallucinations.

Primary Sources

  • Principles for structuring code around business domain concepts and rules. Domain-Driven Design
  • Design patterns for protecting invariants and encapsulation in object-oriented code. Design Patterns
  • Framework for governing AI systems with requirements for domain constraint enforcement. NIST AI RMF
  • Supply chain security framework with requirements for code integrity. SLSA Framework
  • OWASP security risks for large language model applications. OWASP Top 10 LLM
  • SOC 2 governance criteria for control design and enforcement. SOC 2 AICPA

Get Started with Bitloops.

Apply what you learn in these hubs to real AI-assisted delivery workflows with shared context, traceable reasoning, and architecture-aware engineering practices.

curl -sSL https://bitloops.com/install.sh | bash