Four hoardings booked; the system said three were free
A campaign on four hoardings was checked for clashes against one of them, so three could be sold again to someone else. The thing you sell is the face and its dates.
A client books a campaign across four of your hoardings for the same month. Your system records it. A week later a second salesperson checks whether one of those four is free for the same dates, and the system says yes. As far as the clash check was concerned, three of the four surfaces on that campaign were still available to sell.
The cost of that is not a software defect. It is the phone call to a client whose campaign has to move, or the client who turns up to find someone else’s artwork on the site they paid for, and the credit note that follows. A media owner runs on being able to say “that face is taken” and be right. This system could not.
What was actually going on
The system had two different records that were both called a booking. One was a booking header: one client, one date window, one rate, and a single site. The other was a set of per-site booking lines, one for each surface on the campaign, which existed and were plainly meant to be the way a campaign covering several hoardings was recorded.
New bookings were written to the header. The lines were filled in by import and by conversion, and read by the calendar and the availability screens, but the code that decided whether one booking clashed with another never looked at them. It compared header against header, on that single site. A campaign on four hoardings had one header naming one site, and four lines. The clash check saw one.
The unit of sale is the face and the window
The correction is a way of thinking about the business, and it reaches well past outdoor media. The thing you draw on a map is not the thing you sell. A location has an address, a photograph, coordinates, an owner, a lighting type. None of those is what a client buys. A client buys a surface, for a window, at a rate, and one location can carry more than one surface, so each is its own record with its own dates.
Once that is said out loud, several problems that felt separate become one. The clash check belongs on the line, by surface and date window, because that is the record that says something is committed. Availability is a property of a surface over a window, not of a location. The rate belongs on the line too, because that is where a price meets a specific surface and can know whether it is lit. And a campaign on four surfaces produces four jobs for the field team and four sets of proof photographs, which is what operations needed anyway.
Every one of those had been attempted on the header at some point, and every one had a qualifier attached to make it work: take the first site, take the header rate, assume one photo set. When a system needs “take the first” to function, it is recording the wrong thing.
What we changed, and the order we did it in
The first assumption was that the lines were the newer, correct mechanism and the header’s site was a leftover, so the plan was to move the clash rule onto the lines and stop reading the header. That was backwards in the way that matters. The lines were the better record and the unused one for new bookings. The header was the leftover and it was live. Putting the correct rule on the dormant record would have produced a clash check that never ran, on a system whose users had just been told double-booking was now prevented.
So the order became: put the rule where bookings are actually written, mirror the fields onto the lines at the same time so the two cannot drift, then move the writing onto the lines, then remove the header’s site. Three steps instead of one. A rule that is known to be wrong gets worked around by the people using it. A rule that silently does not run gets trusted.
What it did not fix
On the version that shipped, the header’s site field is still present and populated, and one report still reads it. It is correct today because conversion writes both. It stays correct only for as long as nobody adds a third way of creating a booking.
The mechanism, and the symptom that gave it away
The way this surfaced was not a double-booking. It was a booking calendar that came up empty. Not wrong, empty. The calendar drew its bars from the per-site lines, and the screen that listed those lines had been trimmed to return only each line’s identifier and its site, because that was all its first caller needed. Every line arrived without a start or end date, the calendar skipped every line with no dates, and drew nothing. A two-line fix.
That is the shape most modelling mistakes take when they finally show themselves. Nothing failed loudly. A screen drew its frame, its filters, its legend and its headline figures, and the area where the data goes was blank. The header knows about a site; the lines know about surfaces and dates; and any code that reconciles the two does it with an assumption that is silent when wrong.
Decide what a single sellable unit is before writing the first rule, and put every date, rate and clash check on that record. If the answer to “which record means this is committed?” is “it depends which screen you ask”, double-booking is already possible and you have not seen it yet.
That is the record Sazinga AdBoard keeps: a face and its dates, so that when the system says a surface is taken, it is.