AI Writes the Code, You Build the Cage
The More You Use AI, the More Bugs You Get
Recently, I started relying heavily on AI programming, and efficiency really did increase exponentially. But a strange phenomenon also emerged: Bugs increased, and they were the kind of "inexplicable crashes."
Just last week, I used AI to implement the logic for adding a shopping cart button to the header of every page on a website. The AI quickly provided the code, and I ran a few simple positive tests—adding to cart, opening the cart, clicking pay. The flow worked. I confidently merged the code and deployed it. After going live, there were no problems. Then, a few days later, on a weekend afternoon (I was out playing pool at the time), the customer service department suddenly received reports that the web pages online had all crashed. The reason was an error in a shopping cart API call. I hadn't added an ErrorBoundary to the shopping cart button, causing the error to propagate globally. The entire web application directly displayed an error UI.
The code at the time looked something like this:
// layout.tsx
<Suspense>
<CartButton />
</Suspense>
// api throws an error when the interface is abnormal
const CartButton = () => {
const { data } = useSuspenseQuery({
queryFn: () => Promise.all([
api.getCartVideoList(),
api.getCartMusicList(),
api.getCartPhotoList()
])
})
...
}
An ErrorBoundary was missing:
// layout.tsx
<ErrorBoundary> // <-- Missing this; the error propagated globally
<Suspense>
<CartButton />
</Suspense>
</ErrorBoundary>
This rarely happened in the past era of "traditional coding." Before, I typed code line by line, and every conditional branch and error boundary was in my mental model. Now, the code AI writes can work, but its structure, naming conventions, and boundary handling are completely different from mine. During testing, I only verified the "happy path," completely unaware that the AI had missed a "failure boundary" in some corner.
This plunged me into deep reflection: Was my testing too sloppy, or is the code AI writes too complex and doesn't match my reading habits? Or, was the AI's code actually correct, and it's just that my own understanding is lacking, and I can't read what it's doing?
The Essence of Software Engineering: Managing Software Complexity
With these confusions, I returned to the most fundamental topic in software engineering—complexity. I want to use this to clarify: in the era of AI programming, which complexities can be safely handed over to AI, and which complexities must be personally "locked in a cage" by the programmer?
Complexity is usually divided into two types: one is called accidental complexity, and the other is called essential complexity.
For example, you might need to deal with some historical language issues:
For instance, typescript, to maintain compatibility with object key values in older versions of javascript, allows code like this:
const object: Record<string, string> = {}
// type object.d is string, but it's undefined
console.log(object.d)
const NAME = 'name'
console.log(object[NAME])
This code is strange because the obj doesn't actually have a property d. The type is inferred as string, but at runtime it's undefined. This disconnect between type and runtime easily leads to downstream logic errors.
AI can easily generate this TypeScript pattern because it's common in training data. The syntax is correct, but the intent of types as documentation is broken. This disconnect between type and runtime is a classic example of accidental complexity. AI cannot perceive the hidden danger; only a human can guard against it.
For example, in javascript, there are scenarios of implicit type conversion:
function test() {
const a = 0
// const a = []
// const a = ''
if(a) { // a is implicitly converted to a boolean value
// true
} else {
// false
}
}
These are all accidental complexities. A person needs a deep enough understanding of the language and framework to truly avoid or control these complexities. Therefore, the real antidote to accidental complexity is not making AI write better code, but enabling programmers to develop the muscle memory to identify "type disconnects" and "boundary loopholes" during code review. AI paves the road; humans check the guardrails.
The second part of complexity is essential complexity. This means the business requirements themselves are inherently complex. User search, filtering, ordering, payment, permissions, risk control, event tracking, compatibility with historical data—these things are naturally complex. Good software design doesn't make these things simple; it gives complexity ownership, boundaries, and limitations.
Faced with essential complexity, AI's performance is even more inadequate. Because essential complexity contains a large amount of implicit context. Many module functions exist due to historical reasons and business compromises, but AI is unaware of this. If these modules themselves influence each other and complexity leaks out, then all AI can do is make this pile of "legacy mess" code bigger and harder to understand, to the point where you can no longer comprehend its logic when making changes—yet the code still "magically" works.
Therefore, in the AI programming era, defining "change boundaries" has become a mandatory skill for programmers. We are not competing with AI to see who types faster, but to see who can better see the invisible "boundary line" behind the business.
So, how do you give complexity a home? How do you judge whether a group of files belongs to a certain module?
The core viewpoint is to group them by the reason they change. Ask: why would this thing change?
For example:
- Search filtering changes because the product team adjusts search rules (search module)
- Pagination changes because the display strategy is adjusted (UI component)
- Event tracking changes because the data analysis metrics are adjusted (stat module)
- Payment changes because the transaction flow is adjusted (buy module)
If the reasons for change are different but are mixed in the same module, then every future change will pollute each other.
After clarifying module boundaries, the next question naturally arises: What guards the boundaries between modules? The answer is interfaces. But what is the core purpose of interface design?
The answer is to contain complexity. We always emphasize that a module should be highly cohesive; it should handle the complexity of the business itself and prevent that complexity from spreading. Modules should be loosely coupled; both sides should communicate and exchange data using designed interfaces according to a contract.
This leads to a new thought: Are interfaces between different modules abstracted and designed just because external modules need to call them?
We often mistakenly think that abstracting an interface is for the convenience of multiple callers, but this puts the cart before the horse. The core purpose is not reuse, but isolation.
If it were just for reuse, external modules could directly depend on the implementation. But this is not good enough, because implementations are prone to change for various reasons, like switching to a new package to solve a similar problem. However, other business modules might depend on specific capabilities of the old package, making it impossible to guarantee correct operation after the switch.
Therefore, this is also a manifestation of the Dependency Inversion Principle: different modules should depend on interfaces, not implementations.
Furthermore, reuse is often a false proposition. It might be reusable today, but tomorrow a certain feature might be cut. Looking back later, it might just look like over-engineering.
Therefore, when providing an interface, it's more important to think about:
- When this thing is changed, will it affect places it shouldn't?
- Can this business rule appear in only one place?
- Can this module be tested independently?
- Can the outside world use it without understanding its internal details?
So the real value of "low coupling" is not elegance, but reducing mental burden.
We can form our own software design judgment framework:
Layer 1: Business Boundary
What business problem does this module solve?
For example: search, shopping cart, order, payment, user, event tracking, recommendation.
If you can't describe what a module does in one sentence, its boundary is already blurred.
Layer 2: Change Boundary
For what reasons will it change in the future?
If two things change for different reasons, don't easily bind them together.
Don't bind two things that change for different reasons in the same file.
Layer 3: Data Boundary
What data does this module use?
Who can read it? Who can modify it? Where does the state come from? Where does it land?
If the data flow is unclear, complexity has already started to leak.
Layer 4: Interface Boundary
What is the minimum the outside world needs to know?
Which details should be hidden?
An interface is not better when it's richer; it's more stable when it's smaller.
Layer 5: Failure Boundary
If it fails, what is the scope of the impact?
Can it fail locally, rather than crashing the entire page?
If an error in one module can white-screen the entire application, its boundary is out of control.
Returning to the question that initially confused me: The code AI wrote could run, but why did it crash immediately after going live?
My answer now is: AI is our executor; we are its architect.
We should first design the architecture, divide the boundaries, and abstract the interfaces, and then hand the specific implementation over to AI to fill in. But there is one bottom line that must be guarded—never merge a piece of code you yourself cannot understand. If the code is already hard to understand, it means the complexity has not been properly isolated. At that point, you must stop and refactor, rather than letting AI continue to pile more on top.
Good software design has never been about pursuing code-elegance for the sake of cleanliness, but about pursuing: understandable, modifiable, verifiable, replaceable, and capable of localized failure.
Programmers are not here to eliminate complexity—that's impossible. Our job is to lock complexity in a cage.
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
Suggestion: keep using it until you can solve this problem, then write another article [thinking]