The Theory of Constraints
That’s what a coworker called it, after listening to me explain — for the umpteenth time — how systems ought to be built. The name is taken, of course: Goldratt used it for bottlenecks on a factory floor. But we couldn’t come up with anything better, and what I mean by constraints is a different thing entirely — NOT NULL, a cap on upload size, the fact that an order has exactly one recipient.
The idea is simple.
Start a system with constraints and lift them later, one at a time, when a reason shows up. Not the other way around: don’t build a permissive system and then walk around it putting up fences.
Two futures for one column
The easiest way to show this is with a column in a database.
A column with NOT NULL on it from day one leads a boring life. Someone eventually tries to write an empty value, gets an error, and right then — right at that moment, while they still remember what they’re doing and why — decides what belongs there. The decision gets made once, by a person who has the context in their head.
A column without the constraint holds a few thousand empty values by year three. And they aren’t all the same:
- some came in with an import from the old system, which simply had no such field;
- some are records created in the admin panel, where the form didn’t require it;
- some came through the ordinary user flow, and nobody can say why they’re empty anymore.
Adding NOT NULL now is not a one-line migration. It means first untangling those three groups, working out what emptiness meant in each, and finding something meaningful to put in its place. It turns out there’s nothing, so a placeholder goes into the database and outlives everyone.
The difference between the two stories isn’t how careful the people were. Same people. The difference is where the check sits: before the write, or after.
Loosening is cheap, tightening is expensive
Generalize it and you get an asymmetry: lifting a constraint costs almost nothing, adding one is expensive. And expensive not in engineer-hours of implementation, but in the investigation that has to come first — an investigation whose findings nobody can predict.
Look at any loosening:
- a field accepted a hundred characters, now it accepts a thousand — not one existing record broke;
- a parameter was required, now it has a default — not one client noticed;
- a column was
NOT NULL, now it’s optional — everything that worked yesterday works today.
None of them breaks what has already been written. Run any of them backward and you get a question instead: what do we do about what’s already there? That question has no technical answer. It always comes down to somebody’s old decisions that nobody wrote down.
Emptiness becomes a contract
There’s a second reason, less obvious and, I think, more important.
Code written where a constraint is missing doesn’t merely fail to honor it. It starts to depend on its absence.
- If a field can be empty, sooner or later someone builds behavior on that emptiness: empty means draft, empty means not reviewed yet, empty means imported.
- If two users can share an email address, a script shows up that counts on it.
- If an upload form has no size limit, somebody will inevitably be pushing a gigabyte through it, because it worked once and that’s how it’s been done ever since.
I’d known Hyrum’s law for years: with enough users, every observable behavior of a system becomes somebody’s dependency. I always took it to be about public APIs. But it’s about a missing check too — the absence is just as observable, and people lean on it just as hard.
So a constraint added late isn’t “we added validation.” It’s a change to a contract nobody ever wrote, and one everybody has already come to rely on.
“But that’s premature complexity”
This is usually where someone tells me not to over-engineer it. YAGNI, keep it simple, we’ll look at it later.
Fair objection — it’s just about something else. Premature complexity is writing code for a job that doesn’t exist yet: an abstraction for a second payment provider, localization for a second country. A constraint is the opposite move: it’s a refusal to write code. It says “there’s only one case here,” and that removes branches rather than adding them.
A permissive system isn’t simpler than a constrained one; its complexity just isn’t written down. It’s smeared over every spot where somebody later has to work out what to do with a state that should never have existed. A constraint gathers that complexity into one spot and gives it a name.
Where to start
None of this takes foresight. It takes an honest answer about what’s possible right now — not what might be needed a year from now.
- The narrowest type that describes the data. Not a string if it’s an enum. Not an enum if it’s a boolean.
- Required by default. Let optional be the thing that needs a reason, not the other way around.
- An allowlist, not a denylist. A denylist is already incomplete the moment you write it.
- A limit on everything that has a size. The number itself barely matters; make it generous. What matters is that the check exists, because adding the limit later means breaking everyone who has already gone past it.
- Private by default. Public is a promise, and a promise can’t be taken back.
A lifted constraint is an event
The other half of all this is that constraints have to be lifted. Not endured, not worked around — lifted, out in the open, once there’s a reason.
Lifting has a property I value almost more than the effect itself: it’s an event. It has a date, an author, a commit, and with luck a coherent message. When somebody asks two years from now why the system has users with no email address, the answer takes a minute to find: here’s the migration, here’s the ticket about GitHub sign-in, where the address is hidden by the profile settings.
A missing constraint is not an event. Nothing dates it and nothing explains it. The question “Why is NULL allowed here?” will never get an answer — there’s nowhere to look.
Constraints turn out to be a way of storing domain knowledge in executable form, and every lifted constraint marks the moment that knowledge changed. A system built the other way around has no such log at all.
Where the rule breaks down
I don’t want this to sound like a universal law. It has at least three weak spots.
A constraint should express a property of the domain, not something you haven’t thought through yet. “An order has exactly one recipient” is a claim about the business: it’s either true or it isn’t. “We haven’t decided what to do about multiple recipients” is a deferred decision, and baking that into the schema is dishonest — it looks like knowledge without being knowledge. Telling the two apart is easy: try saying the reason out loud.
Some things are as expensive to loosen as to tighten. Storage formats, protocol schemas, URL structures, identifiers that have already made their way into other people’s systems. The asymmetry doesn’t hold there, and the choice isn’t “narrower” but “with room to grow” — a version in the protocol, a field for extensions, a prefix on the identifier.
Rigidity is not a constraint. A hardcoded value that should have been a setting looks similar from the outside but delivers none of the above: it expresses no invariant, nothing checks it, it just gets in the way. A constraint says “you can’t do that.” Rigidity says “I didn’t think about it.” The difference is enormous, but it tends to get noticed late — once you’re already up against it.
In summary
All of this follows from one property of systems: data accumulates, and the past doesn’t get rewritten. A constraint stays cheap exactly as long as there are no records that violate it, and after that the price grows with the database and never comes back down.
Hence a rule in two halves, both of them required. Start with the narrowest thing that describes today’s job. And lift on demand — openly, with a reason, because a constraint with nobody to lift it stops being a constraint over time and turns into rigidity.
It isn’t universal. The whole thing rests on loosening being cheaper than tightening, and where that isn’t true the rule doesn’t hold. But it covers almost every decision made in a project’s first month.
Freedom is easy to hand out. Taking it back almost never works.
Comments 0
No comments yet.