As I was reading this article, that itch in the back of my head kept being triggered. I didn't encounter anything obvious, but I felt like something was missing. Then I remembered a project I inherited at a past job.

tl;dr Good idea, but it can be easy to end up with very complex code flow if best practices/language-specific patterns aren't followed.

I'm not sure if my experience is an example of Negative Engineering success or failure (a bit of both, perhaps). I had inherited a tool that monitored a conferencing environment looking for specific conferences that also were not started and had non-moderators waiting to be admitted[0]. It was a code base written by someone who was not familiar with the language it was written in, resulting in code that was very non-ideal.

I had joked that the fact that the system worked was "an accident". It was a perfect example of "failing it's way to success". Every time a targeted conference was discovered, an exception was thrown. When the exception was thrown, the catch block would retry the request slightly differently. It would then add an invisible (which started the conference), monitor for the conference state to indicate it was started and remove the invisible user from the conference. The add/remove had to be done because the "invisible users" could not join two conferences, so we had a pool that would grow/shrink as needed. Nearly every step of the way, of course, there were many circumstances that were appropriate to retry, to catch and recover and (ultimately) to crash[1].

Now, my approach to this kind of a problem is "if you can check that a request will fail and fix it before making it, do that", so I have the "invisible user pool" ensure it adds users before the pool is empty (and if it is empty, creates/returns a fresh user instead of throwing). Nearly every command that could throw had an invariant that could be confirmed before the call yet the developer opted to let things crash and handle the problems as they surfaced.

You can imagine the kind of Rube Goldberg device each part of this solution was. Part of that was the language and the developer's inexperience with it. Abusing Try/Catch, as this developer did, made the code feel like it was just a bunch of GOTO statements. The fixed solution had nearly as many Try/Catch blocks -- it's a distributed network application, after all, nearly every call had something external that could go awry. The difference was that there was an obvious "happy path" and the "catch" logic had been limited to either "sleep/retry", "abandon" or "fix; returning to the happy path".

On the balance, my experience would suggest that Negative Engineering, when applied exclusively ... works[2]. You just might not be able to explain why it works very easily if it's designed carelessly and (at least in some languages) the patterns you're required to use lend themselves to code that can be challenging to follow.

[0] Not a terribly difficult thing to do but not something that the API provided a way to solve so it involved listening for conferences with the API and reading data from the reverse-engineered database the service used... not a difficult task.

[1] The service had its own supervisor which handled recovery so for application failures (database unreachable/conferencing environment down), the right thing to do was exit.

[2] It logged critical errors left and right, but I wasn't put on the project to fix bugs, I was put on it to add a feature ... something that could no longer realistically be done due to the tech debt/complexity the solution had reached being designed the way it was.