One category of operation deserves different treatment from everything else in a business system: the ones that write to tax documents, stock levels or accounting entries.
Not because they are harder to write. Because they are hard to undo. A list that loads badly gets reloaded. Twelve hundred invoices issued with the wrong numbering is a problem that lasts weeks and involves your accountant.
The rule
Any operation touching money, stock or tax must be runnable in dry run mode, and dry run must be the default behaviour.
The second half of that sentence is the one that counts. If dry run is an option to remember, sooner or later somebody will not remember it — usually on the day the document is urgent and everyone is in a hurry. If it is the normal behaviour, causing damage requires stating your intention explicitly.
$ php artisan invoices:issue --dry-run
simulation nothing written to the database
documents 1,284 ready
anomalies 3 · invalid VAT number · rows 219, 604, 1102
Stopping. Fix the 3 and rerun without --dry-run.
What a dry run has to actually do
Writing "dry run mode" and skipping the writes is not enough. A useful simulation does four things.
It walks the same code. If the dry run takes a different path, it is not testing anything. The difference should live at one point only, as close to the write as possible.
It states the numbers. How many documents, for what total, across what numbering range. These are the figures a person can recognise as right or absurd at a glance.
It lists anomalies with their row. Not "errors were found", but which and where. A message that does not tell you where to look costs more time than silence.
It stops by itself. If there are anomalies, the run ends with a halt, not a warning. The difference between a warning and a halt is that warnings only get read once it is too late.
The part everyone forgets
Dry run mode has to be visible in the interface, not just on the command line.
This is a mistake we made and corrected: a system where sending to an outside service was in dry run mode, but the on-screen message still said "also saved to the external system". The call had never been made. Nobody noticed for weeks, because the software was reassuring.
A system that fails loudly costs an afternoon. A system that fails quietly costs months, and it always surfaces at the worst possible moment.
The worst case: failing without saying so
A defect from the same family, found while reading code that had been running for a long time. A function recorded an alert when a field vanished from the source system, but wrote to a database column that did not exist. All of it sat inside a block that caught errors and sent them only to the log file.
The result: the field vanished, the import carried on with incomplete data, the alert was never saved, the panel stayed empty. The system was saying "all fine" while importing half-filled records.
Hence a second rule, twin to the first: code whose job is to notice failures cannot live inside a block that swallows errors. If the alarm cannot ring, it has to become the next alarm.