"What happens if I create a voucher called P66666666 by hand?"

I asked that on October 23, the morning of the day our referral program went to production. The answer started with: it can't exist. The voucher code is a char(8). PostgreSQL refuses a ninth character, and our admin form stops typing at eight.

Some context. I run backend engineering at GoodBarber, an app platform. Our referral codes are rows in the voucher table that our back office has used for years, a choice explained in a separate post. No migration was allowed for this feature. Every constraint of that table, deliberate or accidental, became a constraint of the referral program.

Eight characters, five other writers

code is the primary key of the voucher table and a column of the usage table, char(8) in both: fixed width, padded with spaces, anything longer refused. Going past eight would have been a migration.

Who else writes to that table? The admin form, where anyone on the team can type a free code of up to eight characters or have one generated, five letters and three digits. The crons of our older CMS that create monthly and yearly vouchers with direct INSERTs. Gamification, on the monolith side. And two Python scripts in another repository, one for leads and one for resellers, each with its own prefix. Any of them can write any eight-character code.

No column could say "this is a referral code", so the code's shape had to. A voucher counts as a referral code when it is attached to an app, can be used at least once, and matches one regular expression. A global voucher, attached to no app, is never mistaken for one, whatever its code.

The first format was P, a digit from 2 to 9, and six characters from an alphabet without the ambiguous ones, like P5CV7AUJ. Then came my question, and the answer was uncomfortable. A voucher typed by hand as P2ABCDEF and attached to an app would be read as that app's referral code: the referral rules would replace its value and decide who may use it, and it would pay credits after each validated use. No generator produces that shape. A person can type it.

My first idea was a prefix nobody would type: PAR_P, followed by seven random characters. That makes twelve, and the column holds eight. We kept the underscore and moved it to second position:

^P_[2-9][2-9A-HJKMNP-Z]{5}$ 

None of the generators above produces an underscore. That leaves about 229 million codes. A person can still type one on an app-attached voucher, and that limit is written down.

The guard that blocked the first test payment

Our ORM has a guard: when a model reads a row that belongs to an app other than the current one, it hides the row. For vouchers, that made sense for years. A voucher belonged to one app, or to none.

A referral code belongs to the sponsor's app and is used from the referee's app. It crosses tenants by design. During the first test payment, the order creation asked the voucher model whether the code was an amount or a percentage. The guard hid the sponsor's row and printed its debug line:

Gardefous Webzine Model Common\Models\CommandeRemise Expected: <sponsor app> / Current: <referee app> 

Only our team sees that line, which is why I saw it and a customer wouldn't. For a customer, the hidden row meant "not an amount", which happened to be right for a percentage, and the payment went through. On my screen, the text went out before the headers and broke the redirect.

The first fix read referral-shaped codes without the guard, and I rejected it. Either a fix changes how existing vouchers are read, or it ties a billing read to the referral format for good. I wanted neither.

The version that shipped doesn't know what a referral code is. It reads the voucher without the built-in guard, then applies the same guard function with the same conditions itself, without printing anything. To check that nothing changed, we created five test vouchers: a global amount and a global percentage, an amount and a percentage attached to the sponsor's app, and an amount attached to the referee's app. We compared the old and new answers for them, for the referral code and for a missing code, in three app contexts. 21 cases, 0 differences.

The cache key that didn't know about the guard

The review of that version found the subtle part. The ORM caches findFirst() results in memory for the request, and builds the cache key from the query, but not from the guard flag. The guard itself only runs when the row isn't cached yet. So an unguarded read stored another app's voucher under the key a guarded read would use, and a guarded read later in the same request would have received it without any check.

Nothing took that path that day: the only findFirst() with those exact parameters was the method we were changing. The fix was one parameter. The ORM accepts an explicit key, so that read now caches under its own, __METHOD__ . ":" . $code. In a test, before the fix, a guarded read placed right after it returned the sponsor's voucher in the referee's context. After the fix, the voucher stayed hidden, and the 21 cases still matched.

What the old decisions decided

None of these constraints was chosen for the referral program. The column width is older than the program, the other writers were already there, and the guard protects a rule that was true for years. Between them, they set the code's length, its second character, and the way one model method reads a row.

What's the oldest constraint in your schema that still decides how new features look?