Let's talk
engineering

The booking rule that blocked eight paying clients, and why it was a bug and not a missing feature

A client sat in a review meeting and said the system would not let him sell one of his own screens. He had a digital display that loops eight adverts in rotation. He had eight advertisers who wanted to be on it. The software let him book one, then returned a conflict for every other client on those dates.

The instinct in the room was to log it as a feature request. Digital screens, slot management, a new module. It went to the top of the list instead, ahead of things that sounded much larger, because it was not a missing feature. It was a wrong rule already in production, quietly refusing revenue.

The rule that was too strict

The create path did one check before accepting a booking: find the first active booking on the same site whose date window overlaps the requested one, and if it exists, return a conflict. The availability endpoint did the same thing in the other direction — for each site, the first overlapping active booking marked it unavailable. A binary flag per site.

That rule is correct for a painted hoarding. There is one physical surface and one advert can be on it. It is wrong for a screen, and the software had no way to tell the two apart. There was a descriptive category field, populated with things like hoarding and unipole, but it was free text used for labelling and nothing branched on it. Nothing in the data model said how many things can be on this at once.

So the fix was two columns and one changed comparison. A behavioural classification — static or digital — defaulting to static so every existing row keeps its current meaning without a data migration step. And a capacity on the digital ones, typically six or eight. The overlap check stops asking “does an overlapping booking exist” and starts asking “how many overlapping bookings exist, and is that number below capacity”. Static sites keep the old behaviour exactly, because capacity one and the old rule are the same rule.

Why the classification is a separate field

There was a real temptation to reuse the existing category field. It already contained the word digital in some rows. Adding a second column that overlaps with an existing one feels like bad modelling.

It was kept separate anyway, and the reason is worth stating as a rule: a field that describes and a field that decides should not be the same field. The category is a label the operator types for reporting and filtering. The moment the booking rule reads it, every value anyone ever typed becomes load-bearing. A site categorised as “LED Digital” behaves one way and “digital LED” another, and nobody who typed those strings knew they were writing code.

The new field is a closed set, validated on write, with a default that preserves the old behaviour. The old field carries on being a label.

The part I got wrong first

My first reading of the codebase said the fix belonged on the per-site booking line, because that structure existed in the schema, had associations declared, and was clearly the intended many-sites-per-campaign mechanism.

It was dormant. The live create path attached a single site to the booking header and never wrote a line at all. The line table was only ever read, defensively, inside one forecasting query wrapped in a try/catch commented “table may not exist”. So a plan that put slot logic on the line would have shipped a rule that never executed, and everyone would have believed digital sites were fixed while the header path went on rejecting the second client.

The plan changed to land the logic on the path that actually runs, and mirror the new field onto the dormant one so the two never diverge when the second path is eventually switched on. That is the cheaper half of a decision people usually skip: if you have two mechanisms for the same thing and only one is live, adding the field to both costs one line and saves a divergence you will otherwise find during a demo.

Availability stops being a boolean

The consequence nobody asks for but everybody needs is that the answer to “is this free” changes shape. For a painted hoarding it is yes or no. For a screen it is three of eight used, five remaining. A booking calendar that draws a site as a solid bar has nothing sensible to draw when the site is half-sold.

Once you accept a capacity, every read surface that showed a boolean has to show a fraction: the availability list, the calendar, the picker in the proposal builder. This is the actual cost of the change, and it is larger than the write path. The write path is one comparison. The read surfaces are everywhere the operator looks.

How this should have been sequenced

The requirements list from that meeting had thirty numbered items across seven themes. Most were genuine additions — purchase orders, job tracking, photo proof, commission rules. This one sat in the same list looking like a peer, and its heading in the gap analysis was rewritten to say so plainly: this is a correctness problem, not a feature gap, and it goes first.

The distinction matters because the two get treated differently by everyone involved. A feature gets scoped, estimated, scheduled and negotiated. A wrong rule in production gets fixed. When the client says “the system will not let me do the thing I do every day”, the honest classification is usually the second one, and calling it a feature is a quiet way of deferring it.

When a user tells you the software forbids something their business does routinely, check whether you modelled a constraint that only holds for one class of the thing. Constraints that were true of every row when you wrote them are the ones that break silently as the inventory diversifies, because nothing errors — the system just says no to money and calls it validation.

The honest limit here: the capacity model still assumes slots are interchangeable and pooled. If a client ever demands a specific position in the loop, or the same advert twice in a rotation, this model has nothing to say about it, and that will be another correctness conversation rather than a feature request.

Working on something like this?

We build this kind of software, and we staff the teams that do.

Get in touch