Journal
Engineering3 min read

How we ship features that touch money without changing anyone's price by accident

A pricing bug is the kind you cannot take back once a card is charged. Here is the discipline we use to add discounts, staff pricing and sales to the booking flow without a single price moving until we mean it to.

BEThe Bookatu engineering team

Most bugs cost you an apology. A pricing bug costs you a refund, a chargeback, and a client who no longer trusts the number on the screen. The stakes are lopsided, so the process around anything that touches money is deliberately slower than the rest of our work. This is the shape of it, using a recent feature as the example: a whole-category sale, where a salon can put every facial on sale at once.

REBUILT ON THE SERVER, EVERY BOOKINGBase priceStaff priceCategory saleBest couponCharged
Every booking's price is built in the same fixed order, and the whole sum is redone on the server.

Off by default, in production, doing nothing

The first line of the feature is a flag. It reads an environment variable and defaults to off. That means we can merge and deploy the code long before it does anything, and every price stays byte-for-byte identical to the day before. There is no big-bang launch to be nervous about, because the launch is a separate, boring step we take once we are confident.

ts
export const FEATURE_LIVE =  process.env.FEATURE_LIVE === "true"; // Read at the one place it matters. When the flag is off, the branch// is skipped entirely: no extra query, no price change, nothing.if (FEATURE_LIVE) {  const pct = await activeCategorySalePercent(orgId, "service", service.category);  if (pct > 0) fullPriceCents = applyCategorySaleCents(fullPriceCents, pct);}
1Behind a flagoff by default2Merge & deploystill off3Audit passes0 money bugs4Flip the flagthe go-liveLive
The launch is a separate, boring step. The code ships long before it does anything.

One function, one job

The arithmetic of the discount lives in a single pure function. It takes cents and a percent, and it returns cents. It has no database, no request, and no clock, which means it is trivial to test every edge of it in isolation: a zero sale, a normal sale, a clamp at the top, a negative that should never happen. When the maths is one small function you can hold in your head, you stop guessing about it.

ts
export function applyCategorySaleCents(  baseCents: number,  salePercent: number | null | undefined,): number {  const base = cents(baseCents);  const pct = Math.max(0, Math.min(100, Math.round(salePercent ?? 0)));  if (pct === 0) return base;  return cents((base * (100 - pct)) / 100);}

Recompute on the server, always

The browser shows a price so the client knows what they will pay, but that number is only ever a display. When a booking comes in, the server loads the real service, resolves the staff price, applies any live category sale, and then works out the discount, all from data it fetched itself. If someone edited the request on the way in, it changes nothing, because the price the card is charged was never the price the browser sent. This is the single most important habit in money code: the trust boundary is the server, and the server does the sum.

BROWSERShows a price so the clientknows what they will pay.A display only. Never trusted.requestSERVER · SOURCE OF TRUTH1 · Load the real service2 · Resolve staff + sale price3 · Apply the best coupon4 · Charge exactly that
The number the browser shows is only a display. The server fetches its own data and does the sum again.

Try to break it before a customer does

Passing tests tell you the cases you thought of work. They do not tell you about the case you missed. So before a pricing flag goes on, we run an adversarial pass whose only job is to find a way to make the money wrong. Separate checks each take a different angle: one hunts for any path that overcharges the client, one hunts for a path that loses the business money, one checks the feature is genuinely inert when the flag is off, and one goes after rounding and boundary drift. A sale stacked on a coupon on a deposit is exactly where a fractional cent hides, so that is where we look hardest.

New pricing featureOvercharges?Loses money?Leaks when off?Rounding drift?Clean sweep → GO
Four skeptics, each a different way to be wrong. Only a clean sweep is a GO.

None of this is clever. It is a flag, a small function, a server that does its own maths, and a habit of trying to break your own work before it ships. Put together, it is the difference between adding a feature that touches money and losing sleep over it.

feature flagsmoney-safe codepricing engine designserver-side validationtrust boundary
Ready to put this to work?

Bookatu gives you a branded booking page, deposits, memberships, gift cards and reminders, with 0% commission on your bookings.

Start free