Where to put the rule: client, server, or database
Every architecture discussion starts with the wrong question. The right question is: through how many paths can this data be written?
- architecture
- validation
- database
- backend
Every team has had this meeting. Someone says "this is a business rule, it has to live in the backend". Someone answers "it's faster to change on the front". Someone, from the back of the room, delivers the verdict: "let the database guarantee it". And the meeting ends with no decision, with an open document that nobody is going to write.
The problem is that the question is badly formed. "Where to put the rule" looks like a matter of taste or dogma. It isn't. It's a question of write surface.
The question that settles the discussion
How many paths exist to write this data?
If there's only one path — a single API, a single service, and everyone goes through it — the rule can live wherever is most convenient for the team. In the first weeks of any product, that's almost always the server.
If there are two or more paths, the math changes. And almost every real system has more than one: the API, the admin panel, the CSV importer the sales team uses, the nightly batch load, the script the intern ran "just once", the migration function, the test seed.
Every rule that lives only on the server is bypassed by every path that doesn't go through the server.
Three places, three different contracts
None of the three is the "right" one. Each has a contract, and confusing the contracts is what gives you a headache.
| Where | Contract | What it's for | When it fails |
|---|---|---|---|
| Client | UX | warn quickly, avoid pointless typing, guide the input | it's never the source of truth: it's a suggestion |
| Server | Domain | enforce a business rule that changes with the market | can be bypassed by whoever doesn't go through it |
| Database | Invariant | guarantee the data never exists in an impossible state | it's the most expensive to change |
The distinction that matters is between a rule that changes and an invariant that must not be violated.
- "An order above R$ 500 needs a manager's approval": a business rule. It changes in January, changes again in March. It belongs to the server.
- "The balance can't go negative": an invariant. It was never different and never will be. It belongs to the database.
Putting a changing rule in the database is expensive: altering the schema in production has ceremony, a window, and fear. Putting an invariant only on the server is fragile: one forgotten write path is enough for the data to become corrupted — and silent corruption is the kind that shows up three months later in reconciliation.
The invariant as the last line of defense
The database doesn't need to know why the balance can't go negative. It just needs to guarantee that it doesn't. In Postgres, that costs one line:
ALTER TABLE contas
ADD CONSTRAINT saldo_nao_negativo CHECK (saldo >= 0);
And if the debit needs to be atomic, the guarantee comes along:
UPDATE contas
SET saldo = saldo - $1
WHERE id = $2
AND saldo >= $1
RETURNING saldo;
UPDATE ... WHERE saldo >= $1 RETURNING resolves in a single statement what many people do in three (read, compare, write) — and for that very reason it survives two simultaneous requests, which is where the three-step version dies.
If the operation returns zero rows, the rule was violated. The code treats it as a business error; the database never became inconsistent.
The classic mistake: three implementations that disagree
The other extreme is validating everywhere and keeping the three copies in sync by sheer discipline. It starts like this:
- client: accepts an 11-digit CPF;
- server: accepts an 11-digit CPF and validates the check digit;
- database: accepts any text up to 14 characters.
Six months later, the client allows 14 characters because someone "improved" the form to accept a mask, the server still validates 11, and the database has become the only place where data enters crooked — through the import. Nobody made a mistake on purpose: the three copies simply led different lives.
The practical rule that avoids this: each check exists exactly once, in the strongest form the path requires. The server has the full validation; the database has only the invariant that must not be violated under any circumstances; the client has only enough not to annoy the user.
A three-line checklist
Before deciding where a rule lives, answer:
- Is there more than one write path? If so, the guarantee needs to be at the point everyone passes through — usually the database.
- Does this rule change with the business, or is it a permanent truth? Changes: server. Permanent: database.
- If this rule failed for a whole day, would someone lose money or real data? If so, the database enters the story, no discussion.
If the three answers point to different places, you've just discovered why the three copies exist — and why two of them will diverge.