Every database architect has heard the sermon: “Keep business logic out of the database.” It’s treated as gospel, carved into stone tablets and handed down from senior engineer to junior developer. But here’s the uncomfortable truth, your data will outlive your application code by years, maybe decades. Those elegant validation rules you wrote in Python? They’ll be rewritten in Rust, then Go, then whatever language gets invented next. Meanwhile, your database sits there, accumulating garbage and inconsistencies because you were too principled to use the guardrails built into PostgreSQL.
The debate around EXCLUDE and CHECK constraints isn’t really about features. It’s about fear, fear of coupling, fear of complexity, fear of admitting that sometimes the database is exactly the right place to enforce the rules.
What These Constraints Actually Do (Beyond the Documentation)
Let’s cut through the academic definitions. CHECK constraints are your last line of defense against garbage data. They’re Boolean expressions that scream “NO” when someone tries to insert a negative price or a start date that comes after an end date. The PostgreSQL docs show a simple example:
CREATE TABLE products (
product_no integer,
name text,
price numeric CHECK (price > 0)
);
But the real power shows up in table-level constraints that span multiple columns:
CREATE TABLE products (
product_no integer,
name text,
price numeric CHECK (price > 0),
discounted_price numeric CHECK (discounted_price > 0),
CHECK (price > discounted_price)
);
That last line is the kicker, it enforces business logic about the relationship between columns. Try doing that with a column-level constraint and you’ll quickly realize why table-level CHECK constraints exist.
EXCLUDE constraints, meanwhile, are the sophisticated cousin that most developers have never met. They’re essentially generalized UNIQUE constraints that use operators other than equality. The classic example prevents overlapping time ranges:
CREATE TABLE bookings (
room_id integer,
booking_period tsrange,
EXCLUDE USING gist (room_id WITH =, booking_period WITH &&)
);
That && operator checks for range overlap. If you try to book Room 101 from 2-4pm when someone already has it booked from 3-5pm, PostgreSQL slams the door shut. No race conditions, no application-level coordination, just atomic integrity.
The “Keep Logic in the Application” Fallacy
The Reddit thread that sparked this discussion reveals a common mindset: developers pushing validation to the application layer to keep things “decoupled.” One commenter expressed wariness about “pushing business logic into the database”, preferring validation before data reaches the store.
This sounds reasonable until you consider reality. Applications have bugs. Manual data fixes bypass validation. Bulk imports use scripts that forgot to check the constraints. Your microservice architecture has three different services writing to the same table, each with slightly different validation logic. The database becomes a dumping ground for inconsistent data, and you only discover the corruption months later when a customer complains.
Another developer in the discussion framed it perfectly: database constraints are data integrity tools, not business logic engines. They don’t generate user-friendly error messages or handle complex workflows. They simply guarantee that no matter what chaos happens upstream, bugs, manual operations, data migrations, your data remains structurally sound.
This distinction matters. CHECK constraints that enforce price > 0 aren’t business logic, they’re data integrity rules. The business logic is whether a 15% discount applies on Tuesdays. The integrity rule is that prices can’t be negative. One changes weekly, the other is invariant.
When Constraints Become Overengineered (The Dark Side)
None of this means you should go constraint-crazy. The same Reddit discussion highlighted a crucial balance: “If I know a certain feature is less well-formed or less critical, I tend to dial down the amount of protection in the DB so that it’s easier to change.”
This is where constraints can become overengineered. Adding an EXCLUDE constraint on a table that sees 100,000 updates per second might kill performance. The GiST index that powers EXCLUDE constraints isn’t free, it adds overhead to every write operation. For high-velocity data where temporary overlaps might be acceptable, that overhead could be a dealbreaker.
CHECK constraints have their own dark side. PostgreSQL assumes they’re immutable, but developers occasionally reference user-defined functions in them. Change that function’s behavior and you’ve just invalidated every row in your table without knowing it. Your next database dump and restore will fail spectacularly because the constraint gets re-checked during restore.
The documentation explicitly warns about this: PostgreSQL does not support CHECK constraints that reference table data other than the row being checked. Violate this at your peril, your database might appear to work, but dump and restore operations will fail when rows load in the wrong order.
Real-World Patterns: Where the Rubber Meets the Road
So what’s actually happening in production? The Reddit discussion reveals a clear pattern: CHECK constraints see moderate use, EXCLUDE constraints remain rare.
Developers use CHECK for obvious validations, positive prices, valid date ranges, non-empty strings. These are simple, self-contained, and rarely change. They’re the low-hanging fruit of data integrity.
EXCLUDE constraints, despite their power, face adoption barriers. They require understanding GiST indexes and operator classes. They need the btree_gist extension for non-range types. Most developers have never seen them in the wild, so they default to application-level coordination, a solution that works until it doesn’t under concurrency.
The multi-tenant scenario highlights this tension perfectly. In a healthcare SaaS platform with 87 enterprise hospitals, you might need to ensure that appointment slots don’t overlap per-provider, per-location, per-tenant. You could implement complex distributed locking in your application, or you could write:
EXCLUDE USING gist (
tenant_id WITH =,
provider_id WITH =,
location_id WITH =,
appointment_time WITH &&
)
One line. Atomic. Correct even when three microservices try to book the same slot simultaneously.
The Trade-Offs That Actually Matter
Martin Kleppmann’s “Designing Data-Intensive Applications” frames this perfectly: “There are no solutions, there are only trade-offs.” The constraint debate isn’t about right vs wrong, it’s about choosing which problems you want to solve.
Database constraints trade:
– Immediate consistency for application complexity
– Performance overhead for data integrity guarantees
– Schema rigidity for long-term data quality
– Vendor lock-in (EXCLUDE is PostgreSQL-specific) for powerful features
Application-level validation trades:
– Flexibility for potential data corruption
– Developer velocity for operational risk
– Technology independence for distributed system complexity
The key insight from the Reddit discussion is that these aren’t mutually exclusive. The most robust systems use both: application validation for user experience and business workflows, database constraints as the final safety net.
Practical Recommendations for the Cynical Architect
If you’re ready to stop treating database constraints like forbidden magic, here’s how to start:
1. Use CHECK constraints for invariants, not business rules
– ✅ CHECK (price > 0) – price positivity is invariant
– ✅ CHECK (end_date > start_date) – temporal logic is structural
– ❌ CHECK (discount_eligible = is_tuesday() – business rules change
2. Use EXCLUDE constraints when concurrency matters
– Booking systems, resource allocation, temporal tables
– Anywhere two users could create overlapping data
– The performance cost is worth the correctness guarantee
3. Name your constraints
CHECK (price > 0) -- System generates meaningless name
CONSTRAINT positive_price CHECK (price > 0) -- Meaningful error messages
4. Document the why
Constraints are schema documentation. A well-named CHECK constraint explains data rules better than a comment that will inevitably become outdated.
5. Test constraint behavior
Include constraint violations in your test suite. Verify that application code handles database-level errors gracefully. Don’t let constraints become surprise failures in production.
The Bottom Line: Your Data Will Outlive Your Code
The most compelling argument from the discussion is simple: “Remember that your data usually outlives your current business logic.” That Rails app you built five years ago? It’s a Django app now. But the PostgreSQL database is still there, accumulating rows, serving queries, and silently accepting garbage if you never told it what “valid” means.
EXCLUDE and CHECK constraints aren’t about pushing business logic into the database. They’re about recognizing that some rules are so fundamental they transcend any single application version. They’re insurance against bugs, manual errors, and the inevitable entropy of large systems.
Are they underused? Absolutely. Most developers never look beyond UNIQUE and FOREIGN KEY. Are they overengineered? Sometimes, when applied to rapidly changing business rules or high-velocity tables where the overhead outweighs the benefit.
The spicy take: your reluctance to use database constraints isn’t about architectural purity, it’s about not understanding them well enough to feel confident. That’s a documentation and education problem, not a technical one. The tools are there, they’re powerful, and they’re waiting for you to stop being scared of them.
Next time you’re designing a schema, ask yourself:
“Will this rule be true five years from now, regardless of what framework we’re using?” If the answer is yes, put it in a constraint. Your future self, debugging a data corruption incident at 2am, will thank you.
For deeper dives into the architectural trade-offs discussed here, explore how foundational database structures support constraint enforcement, or examine the transactional integrity challenges in domain-driven design. If you’re wrestling with multi-tenant scenarios, the PostgreSQL healthcare SaaS dilemma offers relevant war stories.




