If I start with a generic opening like “In the AI age” or “AI is changing software development,” I’d have lost your interest already. You’ve likely read phrases like those a bazillion times in the last month alone. You probably also already have a strong opinion, either in favor of or against AI-generated code. And I am not here to fight you about that.
Instead, I want to talk about the space between the lines of code. The work that doesn’t express itself as lines you can read or as a feature you can click. The work that’s invisible, but that makes the difference between a working system and a system that is nice to work with. The work that is about ownership.
Ownership in this context isn’t about literal or legal ownership. It’s about the human responsibility to understand, maintain, and evolve the code over time. AI can generate code, but it cannot take ownership of it. It cannot understand the domain, make architectural decisions, or bear the consequences of its output. As the owner of a piece of software, you are responsible for the long-term health of the system, and that requires your judgment, thinking about abstraction and coherence, and taking responsibility.
Judgment is the ability to decide what problem is worth solving, at what level, and whether the request in front of you is even the right one.
If you’re a developer, you probably know this situation: You get assigned a new issue, and it’s immediately clear what needs to be done: what the user experiences, what the stakeholder expects, what the product owner envisions. You write the correct code in the correct spot and submit it. The issue is closed, the user is happy, the stakeholder is happy, the product owner is happy, and you’re happy.
For anyone who isn’t a software developer, you might wonder why your software-inclined colleague just fell off their chair laughing. That’s because issues are never that simple. The user might not have described the problem they are experiencing correctly, or they might have some misunderstanding about the behavior, or a weird setup. The stakeholder is thinking about a very specific use case, and the issue from the product owner may have been planned five months ago, by which time half of the assumptions about the system may have changed.
It might also just be technical limitations: it’s not obvious which code path was taken to trigger that error. Maybe it was a race condition, and every code path you’re looking at individually is correct, but the combination is not. Maybe it’s not immediately obvious how a new feature should fit into the existing architecture or how that feature should interact with the rest of the system. Maybe you notice a glaring security hole if you implement a solution as requested, or you need some testing to make sure the solution you implemented is correct and interacts well with the rest of the system.
This is where a developer’s judgment comes into play. You have to understand the domain, the problem, the context, the system, the codebase, and the work required to achieve a certain goal. Sometimes you have to decide whether the request should be implemented literally, deferred, rejected, or reframed as a different product or domain problem. You have to know when implementation is only one step in the system, and when and where optimizations are in order. You have to be willing to say you understand the tradeoffs and endorse the solution.
This is what I personally was thinking of when I saw that MySQL will finally fix bug #11472 after 21 years. The bug was first reported in 2005 and became so infamous that I was sure it was considered a feature until I saw that it was fixed. This bug was the consequence of an architectural decision made in the 1990s, when MySQL implemented the foreign key constraints directly in the InnoDB storage engine. This decision prevented it from intercepting any deletions because of a foreign key constraint.
I think anyone here can imagine that it did not literally take 21 years of constant work to fix this bug. Instead, the maintainers of MySQL likely judged the solution to be too brittle or too costly to maintain. So this bug was left open until the re-architecting of the foreign key constraints in MySQL 9.0 made it possible to fix this bug without introducing new problems. Or perhaps it crossed an abstraction layer in a way that was counterproductive to the project’s goals and direction. This is the kind of judgment that a current AI system is simply not developed enough to make.
While I hope you never have to deal with a bug like this, I think it’s a great example of how important judgment is in software development. It’s not about writing code, it’s about understanding the system and making decisions that will affect the system for years to come.
This facet of ownership directly springs from judgment, but it is, in my opinion, worth considering separately. Once you’ve decided what problem is actually in front of you, abstraction is about deciding what shape the solution should take.
Every software developer has probably heard of the “rule of three” at some point in their career. You write something once and it’s a one-off. You write it a second time and you take note. You write it a third time and you stop, go back, and abstract it. This is where solution shape lives. Is this best handled as a local fix, a reusable component, a configuration point, a new domain concept, or no abstraction at all? The rule is a useful heuristic precisely because it ties the decision to abstract to the experience of friction. You have to write the boilerplate yourself, feel the awkwardness of the API, and notice the inconsistency in the naming before you can decide that something is missing.
In a world where developers’ time is scarce, this pain pushes us to write better abstractions. This not only makes the code easier to write in the future, it also has a very important feature: future changes can be made locally. If you have the same boilerplate sprinkled across the codebase, a change to the underlying concept requires a change in ten places. AI, in this case, acts like a very localized painkiller, and we start treating the symptom instead of the cause while the patient under our blade slowly becomes anemic.
The risk is not just that AI writes bad abstractions. It’s that it makes bad abstractions bearable for longer than they should be, which in turn results in a bigger burden during refactoring when it’s eventually addressed. The risk is that the system drifts into incoherence without anyone noticing, because the friction that would have signaled the problem has been suppressed.
The war with abstraction is actually fought on two simultaneous fronts: Missing abstractions can be bad and hurt a project’s health in the long run. But so can premature abstraction. If you’ve ever generated code, you’ve probably, at some point, noticed AI generating overly abstract code.
I’m sure every developer is familiar with the brutal pain of overly generic code, where everything acts on interfaces and the code that’s actually running was added via dependency injection three abstraction layers down. Understanding the actual behavior of such code is a nightmare. I noticed this in my own use too: AI reaches for abstraction eagerly. Enough that I started experimenting with local agent files just to stop it from treating every small change like a major architectural decision.
You might argue that it doesn’t matter, since AI will be there in the future too to help you clean up the mess. But once enough code has been generated and enough time has passed for it to drift into lots of small inconsistencies, you’ll have to understand the whole system to unify the experience, and that will come with a cost, not just for you, but also for users, who start relying on certain patterns.
Fred Brooks called this conceptual integrity in his book The Mythical Man-Month, drawn from his experience managing the IBM OS/360 project. It’s the feeling that a software product has been developed with a single shared understanding of the system, even when many people contributed to it over time. This requires a shared model of the system and the ability to say “no” to anything that does not fit that model. A system built by many contributors, each solving their local problem well, will be technically correct but conceptually fractured.
Coherence is built from small decisions, most of which are unremarkable in isolation. Consistent naming for the same concept across layers. Restraint in pulling in dependencies. Error messages that match what the product actually does, that tell you what and why something went wrong, not just where in the code it happened. APIs that feel like they were designed by the same person even when they weren’t. None of these things show up in a linter. They accumulate from people who share an understanding of what the system is and what it’s for, and who apply that understanding every time they touch the code.
AI tooling doesn’t threaten coherence by writing incoherent code. It threatens it by increasing code volume faster than teams can maintain a shared model of the system. Each generated fragment can be locally correct, readable, and internally consistent within the scope of the request. But the AI has no model of the system. It doesn’t know that user and account are the same domain concept expressed at different layers. It doesn’t know that your product calls the thing a Workspace and not a Project. It doesn’t know that the team already has a convention for this kind of error or that this abstraction exists two modules over. These failures are small and easy to miss in review. Individually they are defensible. Collectively they are drift.
Of course, a lot of this can be solved with documentation. The same could be said for artisanal, organic, human-written code. But in my experience, no matter the engineering organization, a lot of this understanding is organizational knowledge, built over time and passed down through oral tradition in code reviews, during coffee breaks, or at the water cooler (for any Americans reading).
This matters beyond aesthetics because coherence is what allows a team to keep developing the system at all. Feature development is not limited only by how fast you can write code. It’s limited by how much of the system you can hold in your head, how long it takes to onboard a new developer, how expensive coordination between teams is, how much of a review burden exists, and how often inconsistencies need to be reconciled before you can ship anything. These costs grow with the size of the system and the size of the team. AI tooling can accelerate the code volume side of this equation significantly. The coherence side doesn’t scale anywhere near as much.
Coherence is a problem that scales worse with more code. Reviewing it takes real human time and effort that’s hard to delegate to AI. Most likely AI is not sitting in the strategy meetings (yet?). It didn’t overhear the latest customer complaints and product ideas. So this burden remains on the developer to maintain.
Code generation is a local optimization. Coherence is what makes the sum of all those local optimizations add up to a system someone can still work on.
The least important thing about a piece of code is who wrote it. What matters is who stands behind it, who understands the tradeoffs, can explain the decisions, will notice when the behavior is wrong, and will do something about it. That’s responsibility, and it doesn’t transfer to a tool.
An AI assistant doesn’t revisit a six-month-old architectural decision and recognize it was wrong. It doesn’t read a user report and feel the gap between what was intended and what was experienced. It doesn’t carry the history of what it shipped and why it works like that. It cannot be accountable for the behavior of a system over time, because it has no stake in the outcome and no memory of how it got there. These aren’t minor gaps in capability. They’re the substance of what responsibility means in practice.
I personally am proud of the code I’ve written and the features I’ve shipped. And I don’t mean that in an ego-driven way. I mean that I want to have a stake in how they perform, and that stake changes how I work. Because of it, I love reading user feedback, whether it’s negative, critical, mundane, or informative. I notice small inconsistencies that don’t rise to the level of a bug report, but that I recognize, from having built the damn thing, as wrong.
I’m more likely to prioritize a piece of feedback that seems minor to a product owner but that I know is pointing at something real. That relationship between caring about what you built and being willing to maintain it is what responsibility looks like from the inside. It’s not separable from the act of building it.
This is where AI-assisted development can amplify problems that are already present in organizations without a strong culture of code ownership. Accountability is much easier to diffuse than to assign. The person who produced the feature didn’t write most of it. The reviewer approved what was in front of them. The tool generated what it was asked to generate. None of those things are false, and none of them are sufficient.
What gets lost is the sense that someone, specifically, is responsible for whether this code behaves correctly six months from now. Not because they generated it or reviewed it, but because they understand it and chose to ship it. Without that, you don’t get any particular negligent actor. You get a system where everyone did their part and nobody owns the outcome.
Responsibility is the willingness to live with the code after the moment of generation has passed. Not just while writing it, but six months later when the edge case surfaces, when the user reports the confusing behavior, when the architecture shows its seams.
This entire post has been about work that doesn’t look much like writing code. Deciding what problem is actually in front of you. Reading the signal from friction. Maintaining the shared model across a team. Standing behind what gets shipped. These are not preambles to the real work. They’re a big part of the real work, and none of them have been meaningfully accelerated by AI.
That’s not to say AI tooling produces no genuine productivity value. Scaffolding, boilerplate, local implementation, test generation, and repetitive refactors are real costs, and reducing them frees up developer time. The gains are not imaginary. The question is where they go.
Software delivery is not a single activity. It’s a system of stages, and like any system, its throughput is constrained by its slowest part. As an example, consider a developer whose time is split roughly between 40% writing code and 60% on everything else: framing problems, understanding the domain, making architectural decisions, reviewing changes with judgment, and coordinating with others. Amdahl’s law predicts that a 10x improvement in that part of the work (code generation) reduces the code-writing portion from 40% to 4%, bringing total delivery time down to 64% of what it was. A 100x improvement brings it to 60.4%. The ceiling, the maximum possible gain from accelerating code generation alone, at any speed, leaves you at 60% of the original time.
If we analyze the problem with the Universal Scalability Law, considering the synchronization overhead for all the newly generated code, the maximal gains fare even worse. With more code being generated, the workload of coordinating with the team increases, eating up some of these performance gains in the necessary ceremony around software delivery.
In software delivery, the shared state is shared understanding. Low-level review like syntax, style, obvious errors, can be and increasingly is handled by AI. But the work that follows doesn’t run at the speed of typing, or even of communication. Framing the right problem, making architectural tradeoffs, understanding whether a change fits the existing model of the system, and deciding what a piece of user feedback is actually pointing at, these run at the speed of human thought and understanding. That’s a different and much harder constraint, and it hasn’t changed nearly as much.
This is why ownership matters. Not as a soft concern about professional practice, but as the mechanism that keeps coordination costs down. A team where developers exercise judgment, maintain abstraction, protect coherence, and stand behind what they ship is a team that can actually convert faster code generation into faster software delivery. Without it, you’re accelerating the part of the system that was never the bottleneck to begin with.
Did you find this article interesting? Does it match your skill set? Programming is at the heart of how we develop customized solutions. In fact, we’re currently hiring for roles just like this and others here at Würth IT Italy.