AI coding tools like Claude Code and Codex have completely changed how software gets built. Features that once used to take a week can now be prototyped in a day. Development costs are dropping and delivery expectations are higher than ever before.
For business, that sounds like a win! And in many ways, it is.
But software projects aren’t limited by how fast code gets written but rather how limited the code can be understood, maintained, and extended as the project grows over time.
The feature that ships today may need to be modified 6 months later. A unique bug that happened in production may require a new set of eyes that previously didn’t require because the codebase was “Vibe Coded”. A new business requirement may require the entire flow within the codebase to be analysed. When those happen the question is no longer:
“How quickly can we build this?”
But rather:
“Does anyone understand how it works?”
AI is really good at writing code. What it doesn’t guarantee is that the people maintaining that code actually understands it.
As project requirements grow, features become interconnected. Business rules become more complex and small decisions start having a ripple effect across the entire system.
When people focus on generating code faster, they can unintentionally create a codebase that nobody fully understands.
At first, everything appears to be fine. Features are shipped quickly. Deadlines are met. But overtime, reviews become slower, bug investigations take longer and simple changes become increasingly risky because nobody is completely certain why parts of the system were implemented the way they were.
That’s technical debt – and AI can accelerate its creation just as easily as it accelerates development.
Over the past few months, I’ve been using AI coding tools like Codex and Claude code in my personal projects. One of those projects was a React Native application that was largely built with the help of those tools.
During a routine code review of the related merge requests, I discovered that the application’s background sync would silently stop whenever a user’s authentication token expired.
Nothing crashed and no alerts were reported or triggered.
From the outside, the application appeared to be OK while an important functionality had quietly stopped working, causing a critical bug.
The interesting part is that the code required to prevent the issue had already existed. The AI-generated code never connected that logic to the part of the system that needed it.
Additionally, this bug wasn’t found by automated testing tool or AI analysis, but rather through a simple question raised during the code review:
“What happens if the authentication token expires while the background service is still running?”
Following that thread led to a deeper investigation of the authentication flow throughout the application.
Had this bug reached production, it could have affected every user of the application with little to no indication that anything was wrong.
Identifying the problem was only part of the challenge.
Determining the right solutions required understanding how authentication worked across different parts of the system, how those components interacted with one another, and what impact a change in one area could have elsewhere in the application.
AI tools did help in accelerating the investigation of the bug and suggested several possible approaches. But these suggestions varied significantly in scope. Some addressed the intermediate symptom, while others proposed broader architectural changes.
The challenge wasn’t generating a fix. The challenge was determining which fix would solve the problem given a set of constraints and its impact on the overall system. That required understanding the wider system, understanding the trade-offs of each option, and selecting the best approach keeping reliability and risks of a given fix in mind.
This is where experienced engineering judgement remains essential. AI can help surface possibilities, but deciding what should be built, changed or left alone still requires a deep understanding of both the codebase and the business problem being solved while keeping project deadlines and requirements in mind.
After about 8 months of using AI in my projects, I found that AI can be a good accelerator for our work. They help in speeding up finding bugs and suggesting alternative solutions that we haven’t really thought of.
But none of them mattered until the underlying code path wasn’t properly understood.
As mentioned earlier, the bug itself wasn’t particularly complicated. What made it difficult to spot was that everything appeared to be working correctly on the surface.
This experience was a reminder that even in an era of AI-assisted development or “vibe-coding”, there is still value in slowing down, reading through the workflow end-to-end, and asking questions about what assumptions are valid and what are not.
This taught me several lessons that have consistently helped me get better results from AI-assisted development.
The quality of the output is often limited by the quality of the prompt.
AI generates solutions quickly, but it doesn’t always know the most appropriate path for your application. That context comes from your own understanding with the framework, the existing codebase and business requirements. Each word matters to the AI.
For example, while working on a Laravel application recently, I noticed Claude Code repeatedly steered one implementation path while overlooking a much simpler alternative. The model wasn’t necessarily wrong, it just didn’t have enough context to evaluate all available options.
Recognizing that a better alternative existed comes from years of experience working with Laravel, not from the AI itself.
I’ve found that the more context and direction you provide, the better the AI responses become.
One mistake I made early on was assuming that because a solution looks reasonable, it was probably correct.
In practice, many AI-generated solutions make assumptions about the codebase that simply aren’t true. Before implementing a fix, spend time validating these assumptions against the actual codebase.
This is probably the most valuable lesson that I have learned from using AI for software development.
Many bugs don’t exist within individual components, it’s in the gaps between those components, where assumptions stop being true and the system starts to break.
Whenever I let AI implement a feature, I always make it a point to read through the generated code thoroughly afterwards. I ask myself a very simple question:
“Does this actually make sense?”
If a particular implementation isn’t immediately obvious, I’ll ask the AI to explain why a piece of code exists, what problem it’s solving, and how it fits into the wider system.
The goal isn’t to verify that the AI can explain its own code. The goal is to make sure that I understand it.
That process often reveals hidden assumptions, edge cases, or alternative approaches that weren’t obvious at first glance. More importantly, it helps me determine whether the path the AI chose is the right fit for the application, rather than simply accepting it because it appears to work.
AI has fundamentally changed the way I write software, and I genuinely believe it has made me a more productive engineer.
It helps me investigate code faster, explore alternative solutions, and automate a significant amount of repetitive work. In many cases, it lets me focus more of my time on solving more complex problems rather than writing boilerplate.
But after spending several months working with these AI tools almost every day, I’ve come to appreciate that generating and understanding code are two very different skills.
The token refresh bug wasn’t difficult because the solution was complicated. The solution already existed. The difficult part was understanding the system well enough to notice that something that important was missing.
That’s the part AI still struggles with.
It can generate implementations, suggest fixes and explain concepts. What it cannot do is take responsibility for understanding the trade-offs, why certain assumptions were made, and the consequences of those decisions.
That responsibility still belongs to you, the engineer.
If you’re interested in the engineering process behind AI-assisted development, I would also recommend reading Specification as Source Code: An Engineering View of AI-assisted Development. It explores how clear specifications, architectural constraints, review workflows and automated checks can make AI-generated code more predictable and consistent at scale. Together with the lessons discussed here, it provides a broader scope on both generating software with AI and maintaining confidence in what gets produced.
Perhaps that’s why the most valuable skill in AI-assisted development isn’t prompting, code generation, or even choosing the right model. It’s maintaining enough understanding of the system to know when something doesn’t look right.
Because at the end of the day, the cheapest AI-generated code often is the most expensive code you’ll own if nobody understands how it works.