TL;DR — Constraint checking is the practice of ensuring a system never transitions into an invalid state. The most reliable production systems enforce constraints at every layer: the database, the application, and the governance pipeline. Skipping any layer is how you get the outages that make headlines.
Most engineering teams treat validation as an afterthought — something bolted on to satisfy a product manager’s request for “required fields.” But the teams that ship systems handling millions of transactions per day think about constraints differently. They treat them as first-class architectural citizens, enforced at every boundary, tested as rigorously as business logic, and monitored with the same urgency as latency p99s.
Let’s examine how constraint checking works across the stack, where it fails, and what the best teams do differently.
What “Check Against Constraints” Actually Means
A constraint is any rule that must hold true at all times for a system to be considered correct. Unlike a feature, which describes what a system does, a constraint describes what a system must not become. The distinction matters because features can be iterated on; constraints are the guardrails that prevent iteration from becoming destruction.
Consider a payment system. The business logic might say “transfer $50 from Alice to Bob.” But the constraints say:
- Alice’s balance must never go below zero
- The total money supply in the system must be conserved
- No single transaction may exceed $10,000 without manual approval
- Every transfer must be recorded with an immutable audit trail
These are not features. They are invariants. When they break, the system is not merely buggy — it is wrong, and wrongness in financial systems is measured in dollars, not log lines.
The challenge is that constraints span layers. A database constraint cannot express “no transaction over $10,000 without approval” without a complex trigger. An application-level check cannot prevent a race condition that bypasses it. Only by layering constraints do you get defense in depth.
Database-Level Constraints: The Last Line of Defense
The database is the last system standing when everything else fails. Application servers crash, caches evict, and message queues reorder — but the database persists. This makes it the ideal place to enforce the most critical invariants.
PostgreSQL and the Constraint Arsenal
PostgreSQL offers a rich set of constraint types that most teams underutilize:
NOT NULL— the most basic invariant, yet the most frequently omitted in early-stage startupsUNIQUE— prevents duplicate entities where identity mattersCHECK— enforces arbitrary boolean expressions on column valuesFOREIGN KEY— maintains referential integrity across tablesEXCLUDE— prevents overlapping ranges, critical for scheduling and resource allocation systems
A CHECK constraint can enforce business rules directly at the data layer. For example, a billing table might include:
CREATE TABLE subscriptions (
id UUID PRIMARY KEY,
customer_id UUID NOT NULL REFERENCES customers(id),
plan_type VARCHAR(50) NOT NULL CHECK (plan_type IN ('basic', 'pro', 'enterprise')),
monthly_price NUMERIC(10,2) NOT NULL CHECK (monthly_price > 0),
valid_from DATE NOT NULL,
valid_to DATE,
EXCLUDE USING gist (
customer_id WITH =,
daterange(valid_from, valid_to, '[]') WITH &&
)
);
This schema enforces four constraints simultaneously: valid plan types, positive pricing, referential integrity, and non-overlapping subscription periods per customer. The EXCLUDE constraint is particularly powerful — it prevents a customer from having two active subscriptions at the same time, an invariant that is surprisingly difficult to enforce reliably at the application layer due to race conditions.
When Database Constraints Are Not Enough
Database constraints are powerful but limited by what SQL expressions can capture. Complex business rules — “a user with more than 100 failed login attempts must be locked for 15 minutes” — don’t map cleanly to a CHECK expression. This is where application-level constraints come in.
Application-Level Validation Patterns
Application-layer constraint checking is where most engineering effort goes, and where most mistakes happen. The pattern is deceptively simple: before any state transition, verify that the transition satisfies all applicable constraints. But implementation quality varies enormously.
The Fail-Silent Anti-Pattern
The most common mistake is catching constraint violations and returning a generic error without context. Consider this pattern:
def transfer_funds(from_account, to_account, amount):
try:
if from_account.balance < amount:
raise InsufficientFunds()
# ... proceed with transfer
except Exception as e:
return {"error": "Transaction failed"}
This returns the same error for insufficient funds, a database outage, and a network partition. The caller cannot distinguish between “fix your input” and “try again later.” A better approach returns structured error information:
def transfer_funds(from_account, to_account, amount):
constraints = [
Check("balance", from_account.balance >= amount, "Insufficient funds"),
Check("limit", amount <= MAX_TRANSACTION, "Exceeds transaction limit"),
Check("status", from_account.status == "active", "Account is frozen"),
]
failures = [c.message for c in constraints if not c.passed]
if failures:
raise ConstraintViolation(failures)
# ... proceed with transfer
This pattern — collecting all constraint failures before raising — gives callers a complete picture of what went wrong, which is essential for debugging and for building user-facing error messages.
Schema Validation as Constraint Enforcement
Modern API design treats request schemas as constraint specifications. Tools like JSON Schema, Zod, and Pydantic allow teams to declare constraints declaratively:
# Order creation payload constraints
type: object
required: [customer_id, items]
properties:
customer_id:
type: string
format: uuid
constraint: "must reference an active customer"
items:
type: array
minItems: 1
maxItems: 50
items:
type: object
required: [product_id, quantity]
properties:
quantity:
type: integer
minimum: 1
maximum: 100
This is not just input sanitization — it is constraint declaration. Every field, every boundary, every relationship is explicitly stated as a rule the system must enforce. When the schema is machine-readable, it can be used for automatic documentation, client-side validation, and even test generation.
Policy-as-Code: Constraints at the Governance Layer
As systems grow in complexity, constraints increasingly span organizational boundaries. Security teams need constraints that apply across all services. Compliance teams need constraints that apply to data residency. Infrastructure teams need constraints that prevent resource over-provisioning. This is the domain of policy-as-code.
Open Policy Agent and Rego
Open Policy Agent (OPA) has become the de facto standard for policy-as-code. It allows teams to write constraints as declarative policies in the Rego language, and evaluate them against any JSON-like data structure:
package kubernetes.admission
deny[msg] {
input.request.kind.kind == "Pod"
not input.request.object.spec.containers[_].resources.limits.memory
msg := "All pods must have memory limits set"
}
deny[msg] {
input.request.kind.kind == "Deployment"
input.request.object.spec.replicas > 10
msg := "Replicas cannot exceed 10 without approval"
}
These policies are evaluated at admission time — before a Kubernetes resource is persisted. The constraint is enforced not by the application that created the resource, but by the infrastructure itself. This is a fundamentally different architecture: constraints are no longer in the application, they are around it.
The Shift from Reactive to Preventive
Traditional monitoring catches constraint violations after they happen. Policy-as-code prevents them from happening at all. This shift from reactive to preventive is one of the most significant architectural changes in modern platform engineering.
At companies like Netflix, Spotify, and Airbnb, policy-as-code governs everything from deployment approvals to data access patterns. A developer cannot accidentally deploy a pod without resource limits, because the admission controller rejects it before it reaches the cluster. The constraint is not a reminder — it is a wall.
Architecture Patterns in Production
The most resilient systems share a common architectural pattern: constraints are enforced at every layer, and each layer’s constraints are independent of the others. This is constraint layering, and it follows a simple hierarchy.
- Client-side constraints — Fast feedback, poor security. Used for UX, never for enforcement.
- API gateway constraints — Rate limiting, authentication, request size limits. The first serious gate.
- Application-level constraints — Business logic validation, domain invariants. The richest layer.
- Database constraints — Referential integrity, uniqueness, check expressions. The last line of defense.
- Policy-as-code constraints — Cross-cutting governance, compliance, security. The organizational layer.
Each layer catches what the others miss. A client-side validation might prevent a malformed request from ever reaching the server. The API gateway might block a request that exceeds rate limits. The application layer validates business rules. The database prevents race conditions that bypass application logic. Policy-as-code prevents organizational violations that no single service would catch.
The key insight is that no single layer is sufficient. Teams that rely solely on application-level validation are one deployment away from data corruption. Teams that rely solely on database constraints are one sprint away from a schema migration nightmare. The layers work together.
Common Failure Modes
Despite decades of software engineering experience, constraint-related failures remain one of the top causes of production outages. The most common patterns include:
- Constraint drift — Constraints are added over time but never reviewed. Old constraints that no longer reflect business reality create friction and are eventually bypassed, defeating their purpose.
- Soft constraints treated as hard — “Best effort” validation is implemented as a warning rather than a block. When the warning is ignored (as warnings always are), the constraint is effectively dead.
- Missing constraint tests — Constraints are documented but not tested. Without automated tests that attempt to violate each constraint, there is no evidence the constraint actually works.
- Constraint coupling — A change to one constraint cascades into failures in unrelated systems. This happens when constraints are not isolated by bounded context.
- The “it works on my machine” constraint gap — Local development environments skip constraint checks for speed. Production data reveals that the constraints were never actually enforced in the paths that mattered.
The most expensive outages I’ve studied share a common thread: a constraint existed on paper but was bypassed in practice, and the bypass went undetected for months.
Key Takeaways
- Constraints are not features — they are invariants that must hold at all times, and they deserve the same rigor as core business logic.
- Layer your constraints — client-side, gateway, application, database, and policy-as-code each catch what others miss. No single layer is sufficient.
- Database constraints are your last line of defense — use PostgreSQL
CHECK,EXCLUDE, andFOREIGN KEYconstraints aggressively, especially for invariants that are vulnerable to race conditions. - Policy-as-code shifts constraints from reactive to preventive — tools like OPA evaluate constraints before resources are persisted, preventing violations rather than detecting them.
- Test every constraint by trying to violate it — if you haven’t written a test that attempts to break a constraint, you don’t know if it works.
- Review constraints regularly — constraint drift is silent and deadly. Old constraints that no longer reflect business reality should be retired, not bypassed.
Further Reading
- PostgreSQL Constraints Documentation — The official reference for all constraint types available in PostgreSQL, with examples and performance considerations.
- Open Policy Agent: Policy as Code — The definitive resource for learning Rego, writing policies, and integrating OPA into your infrastructure.
- Designing Data-Intensive Applications by Martin Kleppmann — Chapter 5 covers constraints, invariants, and consistency models in depth, with practical examples from production systems.
- Google SRE Workbook: Implementing Constraints — Google’s internal guidance on how Site Reliability Engineering teams define, enforce, and monitor system constraints at scale.
- JSON Schema Specification — The standard for declarative constraint specification on JSON data, widely used in API design and configuration validation.
- The Philosophy of Software Design by John Ousterhout — A talk on how to think about constraints and complexity in software architecture, drawing on decades of systems research.
Building something like this? I’m an AI/systems contractor open to new projects. Book a 30-min call.