The Demo Worked. The Codebase Didn't

By Charlin Joe · 24 July 20260 views

The Demo Worked. The Codebase Didn't.

The handoff call lasted forty minutes. The agency walked through the app screen by screen — guest checkout, order history, admin panel, returns flow. Everything worked. The client signed off. The agency moved on to the next project.

Six weeks later, the client's new engineering team was still trying to understand what they had inherited.

Not because the code was messy. The code was actually quite readable — consistent naming, reasonable structure, no obvious shortcuts. The problem was something else entirely. Every time they tried to understand why something was built a particular way, they hit a wall. No design documents. No decision records. No comments that explained intent. Just code that worked, and silence where the reasoning should have been.


Why did guest orders live in a separate guest_orders collection when the admin panel clearly expected them in the main orders collection? Nobody knew. The original developer had left. The commit message said "add guest order support." The PR had no description. There was no design document, no decision record, nothing in the codebase that explained the reasoning behind the choice.

They found the bug in week three. A query in the admin panel aggregated order data across both collections — except it didn't, because it was written before guest_orders existed and nobody had updated it. Guest orders were invisible to the operations team. Had been since launch. The client had just never noticed, because they never looked at the operations side of the panel during the UAT. The operations team discovered it when the first guest customer called support asking where their order was.

The fix took an afternoon. Understanding whether the fix was safe took two weeks.

That two-week gap is the part that doesn't make it into post-mortems. It's not dramatic. Nobody is paged at 2am. But it's engineering time — senior engineering time — spent reading code, tracing dependencies, running test suites against scenarios nobody thought to write tests for, and ultimately making a judgment call on whether the fix was isolated or whether it had implications that hadn't surfaced yet.

Two weeks of engineering time. For an afternoon fix. That ratio is what a missing decision record costs.


This is the failure mode nobody talks about when they talk about AI-generated code.

The conversation usually goes: AI wrote buggy code, tests didn't catch it, it shipped. That's a real problem, but it's not the expensive one. Bugs get fixed. What's expensive is a codebase where the reasoning behind the code is gone — where every change requires archaeology instead of engineering, where onboarding takes six weeks instead of one because the new developer can't distinguish deliberate decisions from accidental choices.

The agency used AI tools throughout the project. The code was generated fast, looked correct, passed review. But the AI made dozens of architectural decisions that no human ever signed off on. The separate guest_orders collection wasn't in any spec. It wasn't discussed with the client. It wasn't evaluated against the alternative of a unified collection with a nullable userId field. The AI encountered a decision point, made a reasonable-sounding call, and kept going.

Nobody caught it because nobody was looking for it. The review was a style check, not a conformance check. The tests verified the code's behaviour, not the requirements' intent. CI turned green. The feature shipped.


Here's the thing about that separate collection decision: it wasn't obviously wrong. In isolation, a guest_orders collection makes sense. Conceptually clean. Easy to explain. The problem only becomes visible when you know the constraint the AI didn't know — that operations needed a single workflow, that the admin panel was built around a single collection, that the returns flow used an orderId-plus-email lookup that already worked without modification for registered users and would need a completely different path for guests if the collections were separate.

The AI never knew those things. Nobody told it. There was no document that committed those constraints before code was written. From the AI's perspective, a separate collection was a perfectly valid architectural choice. And in a vacuum, it is. The failure wasn't the AI making a bad decision. It was the absence of a process that required the business constraints to be written down before any decision was made.


It's worth naming what the AI was actually doing when it created the guest_orders collection, because understanding the mechanism is what makes the fix obvious.

AI code generation fills in gaps. When a developer provides a task description — "add guest checkout" — the AI receives a goal and a codebase, not a set of constraints. Everywhere the goal is underspecified, the AI resolves the ambiguity itself, choosing the most locally coherent option. A separate collection is locally coherent: it keeps guest data isolated, makes queries clean, and avoids complicating the existing Order model. None of those are bad engineering instincts. They're just instincts that optimise for the code in isolation rather than the system as a whole.

This is the structural property of AI generation that teams consistently underestimate: the model is excellent at local coherence and unreliable at system-wide constraint satisfaction. It will produce code that makes sense internally, that follows conventions, that is internally consistent — and that violates a business constraint nobody stated, because business constraints don't exist in the code. They exist in conversations, in requirements documents, in the heads of the operations manager and the product owner. If those constraints aren't written down and made available before generation starts, they don't exist as far as the AI is concerned.

The guest_orders collection isn't the only place this happened in the agency's codebase. The new engineering team found fourteen other decisions during their six weeks of archaeology — most of them defensible in isolation, several of them incompatible with constraints the client had assumed were obvious. A GDPR deletion handler that cleaned up accounts but not guest email data. A returns endpoint that assumed userId was always populated. A weekly reconciliation report that grouped orders by user type in a way that silently excluded any order where userId was null. Each one was a reasonable call made from incomplete context, each one invisible until someone bumped into it.

Fourteen gaps. Fourteen separate archaeology projects. Not because the AI was bad — because the process gave it nothing to work from except the task description.


In a disciplined process, the first thing produced for a feature like guest checkout is a business intent — a written record of the problem being solved, the constraints the implementation must respect, and what success looks like from the business side. For this feature, that document would have included one sentence that changes everything: "Guest orders must appear in the admin order management panel identically to registered-user orders. Operations cannot have two workflows."

One sentence. The AI reads it. The separate collection is off the table before a line of code is written.

That sentence doesn't come from a developer's imagination. It comes from a ten-minute conversation with the operations team that happens during business intent review — before requirements are drafted, before any architectural decision is made, before anyone opens a chat window. The conversation surfaces the constraint. The constraint goes into the document. The document goes into the AI's context before generation starts.

That's not a governance overhead. That's the difference between a six-week handoff and a one-week handoff. It's the difference between a bug that hides in production for months and one that never gets written.


The business intent document is not a spec. It's not a requirements file. It doesn't describe how to build the feature — it describes why the feature is being built, what constraints the business operates under, and what success looks like from the stakeholder's perspective. Those three things are what the AI needs before it can make good architectural decisions, and they're almost never in the task description.

A well-formed business intent for guest checkout includes four sections. The first is the problem statement in business terms: 34% of users abandon the cart at account creation. Revenue is leaving at a predictable, measurable point, and the business knows why. The second is success criteria: a user completes a purchase without creating an account, and post-purchase they're offered account creation with their details pre-filled, but it's genuinely optional. The third is business constraints — and this is where the constraint about operations appears, alongside the GDPR email opt-in requirement, the returns-by-email-plus-orderId requirement, and the equal-visual-weight requirement for the checkout buttons. The fourth is user expectations: immediate confirmation email, no friction, no dark patterns.

An AI reading all four sections before generating a single line of code is working from a completely different starting point than an AI reading "add guest checkout." The gap is not about the AI's capability. It's about what the AI was given to work from. The model that created guest_orders could produce a correct unified-collection implementation if someone had written the operations constraint before generation ran. The same model. The same codebase. A different starting document.


The cost argument for this kind of front-loaded documentation is often framed poorly. Teams hear "write the business intent before you write code" and translate it as "more documentation, slower delivery." That's the wrong frame.

The right frame is: where does the ambiguity get resolved? In every feature development, the underspecified requirements get resolved somewhere. Either they're resolved before code runs — in the business intent, in the ADR, in the requirements file — or they're resolved by the AI generating the most locally coherent answer, or they're resolved by the developer who didn't think to ask, or they're resolved by the reviewer who didn't know to look. Or they're not resolved at all and they surface as a production bug.

Every one of those resolution points has a different cost. A constraint resolved in the business intent costs ten minutes of conversation and thirty minutes of writing. The same constraint resolved in a production bug costs the fix plus the investigation plus the verification plus the client conversation plus, in the worst cases, the data remediation. The constraint doesn't change — it was always true that operations needed a single workflow. The only thing that changes is when the development process encounters it.

Front-loading documentation doesn't slow delivery. It moves the cost of ambiguity resolution from an expensive, visible, often-embarrassing moment late in the process to a cheap, invisible, entirely manageable moment early in it.


ANN SDLC — 10-step process diagram

The engineering team eventually rebuilt the guest checkout feature. They used the old code as a reference but had to discard most of the architectural decisions because they couldn't verify which ones were intentional and which were the AI's defaults. Discarding an architectural decision you can't verify is the right call. But it's a call that costs time, and it's a call you shouldn't have to make.

They wrote a business intent first. Then requirements with acceptance criteria. Then an ADR that explicitly chose the unified orders collection over a separate one — and recorded why the separate collection was evaluated and rejected.

The rebuild took eight days. The original feature had taken three.

But the rebuilt feature is the one they can extend. They can add analytics to it without guessing what the collection structure means. They can hand it to a new developer without six weeks of explanation. They can use AI to generate the next guest-related feature and know the model will respect the decisions already made, because those decisions are committed to the repository in a form the AI can read as context.

When a new developer joined the team three months after the rebuild, they spent their first day reading the business intent, the ADR, and the requirements file. By the end of that day, they understood why userId was nullable, why there was no guest_orders collection, what the access control rules were doing, and why the GDPR deletion handler handled guests by email rather than by uid. They started contributing in week two.

That's the other half of the handoff story. The agency's handoff took six weeks of archaeology and still left gaps. The rebuilt feature's handoff took one day. The difference wasn't the new developer's skill. It was the presence or absence of a paper trail between the problem and the implementation.


There's a specific objection that experienced developers raise at this point: "We've always worked this way — no formal documents, just good developers making good decisions. The code was fine before AI. Why is this suddenly necessary?"

The objection is reasonable, and the answer is about speed and volume, not about developer quality.

In a traditional development process, architectural decisions are made by developers who carry the system context in their heads, discuss options informally, and communicate through code review and conversation. The decision about where to store guest orders gets resolved in a hallway conversation or a Slack thread, and the developer who makes the call understands the implications because they've been on the project for six months. The process is informal, but the humans involved have the context that makes it work.

AI generation breaks this. The model has no project history. It has no hallway conversations. It has no six months of context accumulation. Every generation session starts from whatever is in the context window. The same decision that a senior developer would make correctly from accumulated context gets made by the AI from the task description alone — which is almost always insufficient.

The documentation isn't compensating for a weaker process. It's compensating for the absence of the institutional knowledge the AI doesn't have and can't accumulate. Writing the business intent and the ADR before generation is how you give the AI access to the decisions that a human would carry in their head — decisions that, without documentation, exist only in someone's memory or not at all.

This is also why AI-assisted development appears to work fine at first and degrades over time on teams that don't adopt structured documentation. Early features are simple. The AI's defaults are often correct. The context window contains enough — the codebase is small, the decisions are obvious. But as the codebase grows, as constraints accumulate, as architectural choices from earlier features constrain later ones, the AI's defaults become increasingly wrong. The gap between what the AI infers from the code and what humans know from project history widens with every feature. Without documentation, that gap is invisible until it surfaces as a production incident.


The demo worked. It always works. The question is what happens the week after launch, when the real engineering starts — when the operations team tries to run a report, when a customer calls support, when a new developer tries to understand what they're looking at and there's nothing there to tell them.

The agency shipped software. The new team built a codebase. Those are different things, and the difference is visible in every debugging session, every onboarding, every feature extension that either takes one day or takes three. AI makes the shipping faster than it's ever been. The discipline that makes it a codebase worth inheriting is still a human responsibility. It always has been.


Comments

No comments yet. Be the first!

Sign in to leave a comment.