Chapter 7: Contracts, Schemas, and System Invariants
By Muhammad Riaz
The Silent Corruption of Loose Interfaces
When two human software developers agree to build two halves of a feature—for instance, a frontend engineer building a dashboard and a backend engineer building the supporting REST API—they rarely jump straight into writing code. They first sit down and agree on an API response structure: key names, data types, nullability, and status codes.
Without this upfront agreement, the frontend developer expects a payload structured as { user: { id: "123", name: "Alice" } }, while the backend developer emits { data: { user_id: 123, full_name: "Alice" } }. The result is instant runtime breakdown upon integration.
When working with AI coding tools, this problem multiplies exponentially.
If you ask an AI model to generate a service layer without providing strict structural contracts, it will invent its own interface types, payload formats, and error structures. When that service interacts with existing database models or third-party webhooks, these subtle interface mismatches cause silent runtime corruption —code that compiles successfully, passes naive unit tests, but fails catastrophically under real application workloads.
To build deterministic, integration-ready software with AI agents, you must master Contracts, Schemas, and System Invariants.
The Three Pillars of Technical Precision
In Spec-Driven Development, a specification is incomplete until it defines three fundamental boundary mechanisms:
┌─────────────────────────────────────────────────────────────────────────────┐
│ CONTRACTS, SCHEMAS & INVARIANTS │
│ │
│ 1. INTERFACE CONTRACTS ──> API shapes, status codes, DTO types │
│ 2. DATA SCHEMAS ──> Database constraints, JSON schemas, Zod types │
│ 3. SYSTEM INVARIANTS ──> Rules that MUST hold true in ALL states │
└─────────────────────────────────────────────────────────────────────────────┘
1. Interface Contracts: Defining Exact Boundaries
An Interface Contract establishes the precise payload shape, headers, and HTTP status codes exchanged between components. Instead of describing API behavior in vague prose, a production .spec.md provides explicit language-agnostic schemas (like OpenAPI/Swagger, Protocol Buffers) or strict TypeScript Data Transfer Objects (DTOs).
Real-World Practical Example: Payment Webhook DTO
Suppose an AI agent is tasked with implementing a webhook endpoint that receives subscription updates from an external payment gateway.
Instead of prompting: "Process incoming subscription payment webhooks," the contract is defined explicitly in the spec:
## Interface Contract: Payment Webhook (`POST /webhooks/billing`)
### Request Headers Required
- `stripe-signature`: string (MUST be validated against secret `process.env.STRIPE_WEBHOOK_SECRET`)
### Request Payload Schema (Strict DTO)
```json
{
"id": "evt_1N...",
"type": "invoice.payment_succeeded",
"data": {
"object": {
"customer": "cus_12345",
"subscription": "sub_67890",
"amount_paid": 4900,
"currency": "usd"
}
}
}
Response Status Matrix
-
200 OK: Payload verified and successfully queued/processed. -
400 Bad Request: Payload missing required fields or signature validation failed. -
422 Unprocessable Entity: Unknownevent_typeoutside supported handling enum.
When an AI agent receives this explicit contract inside its specification file, it cannot guess payload structures or invent arbitrary response codes.
---
### 2. Data Schemas: Locking Down Types and Nullability
One of the most frequent sources of bugs in AI-generated code is **loose type safety**—specifically regarding `null`, `undefined`, optional fields, and string-to-number conversions.
If an AI tool interacts with a database or external API, it must be provided with exact schema definitions and validation rules (using toolkits like Zod, Pydantic, or TypeBox).
#### Example: Schema Validation with Zod
In your specification document or linked plan artifact, lock down runtime validation rules:
```typescript
// Explicit Zod Schema defined in Spec/Plan artifact
import { z } from 'zod';
export const CreateUserSchema = z.object({
email: z.string().email({ message: "Invalid email address format" }),
role: z.enum(['ADMIN', 'MEMBER', 'VIEWER']),
organizationId: z.string().uuid({ message: "organizationId must be a valid UUID v4" }),
metadata: z.object({
theme: z.enum(['light', 'dark']).default('light'),
notificationsEnabled: z.boolean().default(true),
}).optional(),
});
export type CreateUserDTO = z.infer<typeof CreateUserSchema>;
By pointing the AI agent to this schema file as a Read-Only Contract Context, any code generated for controllers or service modules must parse input directly through CreateUserSchema.parse(input).
3. System Invariants: Non-Negotiable System Rules
An Invariant is a mathematical or business condition that must evaluate to true at all times, before and after any state transformation.
While contracts define shapes and schemas define types, invariants define fundamental business truth. They prevent an AI agent from creating valid syntax that violates critical domain rules.
Common System Invariant Categories
| Invariant Category | Real-World Business Rule | Failure Consequence if Omitted |
|---|---|---|
| Financial Integrity | account.balance MUST NEVER drop below $0.00 during transfers. | AI processes overdraws without rejecting transaction. |
| Tenant Isolation | All queries MUST contain WHERE organization_id = current_org_id. | Cross-tenant data leak across enterprise accounts. |
| State Transitions | An Order in state CANCELLED CANNOT transition to SHIPPED. | AI allows illegal order processing on dead state. |
| Concurrency Safeguard | Inventory stock updates MUST use atomic atomic decrements or database locks. | Race condition under concurrent checkout requests. |
How to Write Invariants in .spec.md
System invariants must be written as explicit assertions:
## System Invariants & Non-Negotiable Rules
- **INV-1 (Atomic Balance Check):** When deducting user credits, the database update query MUST execute conditionally:
`UPDATE users SET credits = credits - :amount WHERE id = :userId AND credits >= :amount;`
If zero rows are affected, the service MUST throw a `InsufficientCreditsError`.
- **INV-2 (Idempotency Contract):** Every API request carrying an `Idempotency-Key` header MUST yield the exact same response when executed multiple times with identical payload, without re-executing business logic or database writes.
Contract-First Prompting: A Practical Before & After
To understand the difference in execution quality, consider an AI agent given a task to update a user's subscription tier.
Loose Prompting (Without Contracts/Invariants)
-
Prompt: "Write a function to upgrade a user's account tier in our database."
-
AI Output: Writes a simple SQL query
UPDATE users SET tier = 'PRO' WHERE id = userId. -
Disaster: Fails to prorate remaining subscription days, ignores database transaction bounds, and fails to check if the user account is suspended or soft-deleted.
Contract-First SDD Approach
- Spec Input: Points to
.specs/003-tier-upgrade.spec.mdcontainingUserTierDTO, explicit status transition matrix, andINV-1
(Soft Delete Enforcement).
- AI Output:
-
Wraps update inside an explicit database transaction (
BEGIN...COMMIT). -
Applies soft-deletion check
WHERE id = userId AND deleted_at IS NULL. -
Validates request input against
UpgradeTierSchema. -
Generates an integration test verifying that upgrading a soft-deleted user throws a
404 NotFoundException.
Practical Exercises: Contract & Invariant Enforcement
-
Audit an Endpoint for Missing Contracts: Pick an existing REST or GraphQL endpoint in your repository. Check if optional/nullable fields are explicitly defined in a schema file. If an AI model were to regenerate this route today, where would it guess wrong?
-
Write Three Invariants for Your Core Domain: Identify the single most sensitive part of your application (e.g., checkout flow, permission checks, data export). Draft three explicit invariants using the format
INV-1: [Condition MUST always hold true]. -
Execute a Contract Test: Create a
.tsor.pyfile containing only Zod or Pydantic schemas for a new feature. Instruct your AI agent:
"Generate the controller logic for this route. The input MUST be parsed using this schema file, and no unvalidated fields may be
accessed." Observe the accuracy of the generated code.
Next Steps
Now that you can write structured specification files containing airtight contracts, boundaries, and invariants, you face a crucial question: How do you know your specification is completely free of gaps before you start generating code?
In Chapter 8, we will explore The Interactive Clarification Loop: Using AI to Critique Your Specs —learning how to turn AI models into adversarial architects that audit and stress-test your specifications before a single line of implementation code is generated.