← Logs

Let the database keep the promise

The core rule of any scheduling system is short enough to say in one breath: no two sessions overlap for one tutor. On the licensure and tutoring platform I work on, that rule is enforced by one line of DDL — and the choice of database was made for that line. It held through a complete rebuild of everything around it.

The constraint the database can simply guarantee

PostgreSQL has exclusion constraints. Backed by a GiST index, an exclusion constraint says: no two rows may exist where this column is equal and that range overlaps. For scheduling, that’s “same tutor, overlapping time range — refused.” The database checks it on every insert and update, under every isolation level, from every code path, including the ones you haven’t written yet.

That last part is the point. An invariant enforced in application code holds only in the code that remembers to enforce it. The admin panel, a seeder, a background job, a hurried data fix in a console — each is a path that can forget. A constraint in the database can’t be forgotten.

What it would have cost elsewhere

Expressing the same invariant on MySQL would have meant approximating it: a fifteen-minute slot grid, a row per grid cell, hold tokens while a booking is in flight, and logic to decide when a hold is promoted versus when a new one is acquired. All of that machinery exists purely to approximate something PostgreSQL states directly — and every piece of it is a new place for a bug, plus a granularity that real lessons don’t follow.

So “we’re on PostgreSQL” wasn’t a hosting preference. It removed a subsystem.

Choose the store for its guarantees

The general lesson is about how to pick a database. Feature checklists and benchmarks get the attention, but the question that pays off longest is: which of my invariants can this engine guarantee by itself? Uniqueness, foreign keys, check constraints and exclusion constraints are promises the database keeps for you, forever, at no further cost in your code.

Survive the rebuild

The platform has since been rebuilt from scratch — a Laravel modular monolith became a NestJS one — and the constraint moved across unchanged. In the rebuild it isn’t a migration someone wrote once; it’s a permanent CI job against the pinned PostgreSQL image: back-to-back bookings must be accepted, an overlap must raise 23P01, and unbounded ranges must be refused. If an upgrade ever changes that behaviour, the build goes red before anything ships.

The rebuild makes the same move with its module boundaries. Each of seventeen modules owns its own schema, and a database test fails if any foreign key crosses one — so “modules only talk through their public interfaces” is checked by the database too, not left to code review. Different layer, same idea: put the rule where it can’t be forgotten.

The principle

The cheapest invariant to maintain is the one you never enforce yourself. If the database can keep the promise, let it.