Why Clean Code Still Matters in the Age of AI-Generated Code

Recently I caught myself doing something I wouldn't have let slide in a code review.

I'd asked an AI assistant to scaffold a Symfony service. It came back with something reasonable: correct structure, working logic, passing tests. I skimmed it, made one small change, and pushed it. Didn't think twice.

Some days later I was in that file chasing a bug and I genuinely couldn't follow what it was doing. Not because it was broken. Because it had no voice. No intent. It was code written by something that doesn't have to maintain it afterwards.

That's what got me paying closer attention to what I was actually shipping.

I've been writing PHP for the better part of 19 years. I've watched the language go from something developers apologised for to something they actively choose. I've seen frameworks come and go. And I've learned, slowly and the hard way, that making things work has never really been the hard part. Making things understandable is the hard part. Right now, I think that skill is quietly getting deprioritised.

Working and good are not the same thing

AI is genuinely impressive at producing working code. I don't want to be unfair about that because it would be dishonest. The tools are fast, they know a lot, and they save real time.

But there's a gap between code that works and code that's good, and AI sits firmly on the working side of it. It doesn't know your domain. It's never been burned by a badly-named variable at 11pm on a Friday. It hasn't had the argument with a colleague about where business logic should live, or watched a codebase become unworkable because nobody drew clear boundaries early enough.

So it reaches for $data and $result and processItems(). It writes methods that fetch, filter, transform and fire off side effects all in one go, because that's the most direct path between the prompt and the output. It doesn't stop to ask whether a class is taking on too much, because responsibility isn't something it carries.

None of this is a knock on the tools. It's just a description of what they are. The problem is when we forget the gap exists.

You own what you merge

The thing I keep coming back to is this: the moment AI-generated code lands in your repository, it's yours. Not the model's. Yours.

The architectural shortcuts it took? Yours. The test that only covers the happy path? Yours. The class that quietly violates the single responsibility principle because it was easier to generate that way? Also yours.

I think of it a bit like buying a second-hand car. You're still going to look under the hood before you take it on the highway. Not because you assume it's broken, but because you're the one driving it.

Teams that are moving fast with AI are, in some cases, quietly building up technical debt at the same speed they're shipping features. The code works, the sprints look great, and then six months in the codebase starts fighting back. Every change requires understanding ten things that should only require understanding one. That's when the bill arrives.

Working code that nobody understands is not an asset. It's a liability with passing tests.

The fundamentals don't go away. They just move.

What I've found is that the clean code principles I've been applying for years don't matter less in this new workflow. They matter more, because now they're the filter rather than the output.

Naming. AI leans toward generic names because it has no context for your specific domain. It doesn't know that in your system, a User isn't just any user. It might be an authenticated account holder with a billing state, an access tier, and a set of permissions that matter a lot. When I rename $filteredUsers to $activeSubscribers, that's not cosmetic. That's the difference between code that speaks your domain's language and code that could have come from anywhere.

Function size. Ask AI for a service method and you'll often get one that does everything in one shot, because that's what you asked for. It's trying to be helpful. But that's not how I want my Symfony services to look. Each of those concerns deserves its own home. Splitting it apart isn't refactoring for the sake of it. It's making the code navigable.

Architectural boundaries. AI has no idea what your architecture looks like. It will reach across layers without hesitation, inject infrastructure into your domain, and build a class that answers the prompt perfectly but fits nowhere cleanly. This is where knowing your own system becomes genuinely important. You can see what the AI can't.

If you're reviewing AI-generated code and you're not running quality tools in CI, you're leaving a lot to chance. Static analysis catches what tired human eyes skip when the code looks reasonable at a glance. In an AI-heavy workflow it's not optional.

The architect's eye

The framing that's helped me most is this: my job is the blueprint, not the bricks.

Before any AI-generated code lands in my codebase, the important decisions are already made. What are the layers? Where does business logic live? What does the domain language look like? What are the rules around dependencies? Those decisions are mine. They come from understanding the system, its history, and where it needs to go. The AI has no access to any of that. It works from the prompt, not from the architecture.

So when I review AI output, I'm not just checking whether it works. I'm checking whether it fits. Does this class belong in this layer? Does this name match the language we use in this domain? Does this dependency point in the right direction? Those are architectural questions, and they require architectural knowledge to answer. That's the thing the AI genuinely cannot bring to the table.

This also changes what "good review" means. It's less about catching bugs and more about enforcing coherence. Does this fit the system we're building? If not, it goes back, however cleanly it was generated.

The AI can build a room. Only you know what building it belongs in.

Think of it as a very fast junior

The mental model I've landed on: AI is an exceptionally fast junior developer who's very good at syntax and not yet great at judgment.

I wouldn't merge a junior's PR without reading it carefully. I wouldn't assume their naming matched our conventions or that they'd thought through the wider architectural implications. I'd review with genuine curiosity, not looking for reasons to reject but making sure what goes in actually belongs there.

That's the posture I try to bring to AI output now. Not suspicion, just attention. The tools have earned a seat at the table. They haven't earned the right to skip review.

And the better your architectural instincts are, the faster and more accurate that review gets. You can't enforce boundaries you haven't defined. You can't catch a misplaced dependency if you don't have a clear picture of where things are supposed to live. The fundamentals aren't the thing you learn before the real work starts. They are the real work.

To wrap up

I want to be clear: I'm not skeptical of AI tools. I use them every day and I'm not going back.

What I am is someone who's seen what happens when a team prioritises shipping speed over code quality. The codebases that become genuinely painful to work in don't usually get there through one big bad decision. They get there through a thousand small ones, each of which seemed fine at the time.

"It passes the tests" was never the bar. It's still not.

Code gets read far more than it gets written. It gets maintained by people who weren't there when it was made. The decisions baked into it compound over time.

AI changed who writes the first draft. It hasn't changed any of that. Write for the reader.