Happy paths, unhappy truths: AI and the cost of skipping failure

I will first clarify my position. IT has much lower standards than many other industries, such as healthcare, civil engineering, or accounting. There isn’t mandatory training, ongoing education, or certification. IT changes quite often, although frequently only in marketing materials. With a lot of new people coming into the field, we tend to get the average IT project done by a team with, on average, only a few years of experience at best, who have never worked together and frequently don’t even have any domain experience. Let’s not forget that most technology projects or systems never see the light of day, or deliver much less value than promised.

In a way, we build software systems in an absurd manner. Imagine you’re getting your new house built after decades of savings, and you randomly hire 10 tradespeople and engineers who, on average, have only done DIY and helped out on a few building sites.

Think about the idea of your own house. Would you prefer a well-run construction company with a team of people who’ve been building similar houses for the past decade, or just anyone, as long as it’s cheap?*

On average, we have inexperienced people who’ve never worked together, with very little to no domain knowledge. Sounds strange, doesn’t it?

It’s probably obvious that it’s not going to work, for a lot of reasons. What I want to focus on is one reason we’ll now face much more with AI coding and AI embedded in software: building systems for the happy path is fairly cheap, especially if you don’t think about how the systems need to fail.

What do I mean by a happy path? Let’s assume you build a payment system that works only for the case where there’s never any problem. What you’re assuming is something like this list (thanks, AI, for putting the list together):

  • Requests never time out
  • The network connection never drops mid-transaction
  • The payment gateway/processor/bank API is always up and responds
  • Every request is delivered exactly once (no duplicates, no drops)
  • Retries are never needed, so you never have to worry about duplicate charges
  • The operation either fully succeeds or the request never happened — no partial failures (e.g., money leaves account A but never arrives at account B)
  • No concurrent transactions on the same account (no double-spend, no race conditions)
  • The system never crashes between steps of a multi-step transaction
  • Idempotency is irrelevant because nothing ever gets retried or replayed
  • The card/bank/payment method is never expired, stolen, or declined
  • No fraud, chargebacks, or disputes ever occur
  • No regulatory/compliance checks fail (AML, KYC, sanctions lists)
  • Currency conversion rates are always available and accurate, if relevant
  • The user never cancels mid-transaction
  • The user never double-clicks “pay” or submits the same payment twice
  • Input is always well-formed (correct card number format, valid expiry, etc.)
  • Logs/records are never lost, so you never need to reconstruct what happened
  • You never need to reconcile your records against the bank’s/processor’s records
  • No transaction ever needs to be reversed, refunded, or corrected

I think you get the point! The happy path is what a vibe-coded, one-shot app covers, a functional payment system is far more complex.

It’s fairly cheap to build the happy path, however you spend several times more building out how the system should handle failure afterwards. It’s also the way inexperienced people tend to see the world. You’re unlikely to think about how systems should fail, or how people will actually work with them, because that knowledge simply doesn’t exist within your team.

This is where we have a misconception about AI coding and the applications that are now easier to build than ever. We build them for the happy path. Not every application has a lot of unhappy paths though. Your to-do list app doesn’t really have unhappy paths. A payment system, on the other hand, is pretty much all unhappy paths. It’s a lesson a lot of people will hopefully learn, as we always have. With more inexperienced people vibe-coding, we’ve eroded the collective knowledge a little further, though hopefully it allows more domain knowledge to come in over time.

The other challenge is using LLMs within your systems. I think one of the big reasons we’re stuck is the gap between happy and unhappy paths. Large part of the premise of an LLM is that you don’t need to deal with unhappy paths, because it will take care of them. It will just know what to do, since it can take in a lot of context and figure out the right thing to do. You don’t have to explicitly implement all the unhappy paths that can happen, because you leave that to the LLM.

That’s partially true; however, you now have new unhappy paths. What if the LLM gets it wrong? We know LLMs will get things wrong from time to time, in unpredictable ways. That unpredictability makes the unhappy paths hard to see and manage. You effectively need to build a system that’s incredibly paranoid. Instead of building a state machine for what should happen after a payment times out (e.g., retry), you end up introducing a human-in-the-middle for everything, because something goes wrong 5% of the time, but we can’t precisely describe what that “something” is. That’s where we get such a big boom in tools for AI observability and explainability. We skirt around the fact that LLMs sometimes get things wrong, and always will. The more you leave AI to solve your problem, the more unknown issues you’ll encounter, the more you will have to paranoidly manage.

And frequently, once you account for how people will actually use the system, you can end up with something expensive, clunky, and inefficient, because everything has to be checked by a person. This will change as we learn where it’s good to use LLMs and where it isn’t. Again, to get there, you want teams of people with experience, domain knowledge, and an understanding of how people actually work. We underestimate how often we design systems without much thought for how they actually need to function. On top of it we engineers also tend to be biased toward the new toys we want to use, rather than the right, if boring, solution to the user’s problem.

So what’s the actionable takeaway here?

Use AI for prototyping and to identify what the unhappy paths might be in your domain, then check them with your users. It’s a great start. Use AI as part of development to keep the team disciplined about architecture and coding standards, and to develop isolated pieces of work.

Find people with experience in the domain, in user operations, and in the technology. Build a team that covers this in some shape, and understand when they’re going astray. Let them learn. Design for the unhappy paths, write them down, talk about them. You don’t have to implement them from day one, but more often than not, they aren’t considered at all. Build a boring MVP with a clear understanding of how expensive the further features might turn out to be.

Accept that you’ll get the system with the happy path live, and then spend the next few years mostly managing the unhappy ones. If your system is successfully used.

*This deserves its own post, because it’s surprising how many people who’ve worked in the corporate world for 20 years, and have seen dozens of failed IT projects and transformations against very few successful ones, will still throw that experience out the window when it comes to signing off on the next project. Or they get dragged into a project set up the same way as the last ten that failed projects and transformations and very few successful one, will still throw that experience out of window, when it comes up signing off the next project. Or they are dragged into a project that is set up the same way as the past ten that fails.

Leave a comment