Your intuition for your software decays when you stop making decisions
Coding agents today all follow the same workflow. You give them a problem statement and they provide you a plan built around a specific solution. You iterate on that plan, they implement it and you review the resulting code. In this workflow, you’re a mostly passive participant, focusing on review rather than actively designing. Every time the design happens without you, your intuition for your software and its domain decays. This happens because the workflow doesn’t ask the questions integral to a good design process.
Are you solving the right problem?
The same problem can be framed several ways with some framings being easier to solve than others. Working through a problem gives you a better appreciation of the broader context the problem sits in.
In an earlier iteration of rubberduck, we had two services: the main app and the agent service. The agent service had to stay alive for the entire agent loop, which forced us to use a more expensive billing model that kept the instance alive outside of a request. There was a cheaper approach that only kept the instance alive for the duration of the request. Through exploring that solution, I realized that if the user left the session, the request to the main app and thus to the agent service would also close. That meant the agent would stop working. So the problem shifted from using a cheaper billing model to ensuring the user could do other things while the agent kept working. Our existing design already solved that. Nothing needed to be built.
If we had given that problem to a coding agent, it would have provided a solution, which I probably would have accepted. The process of exploring the solution forced me to think through all the data flows and the product as a whole. That’s what showed me no solution was necessary.
Was this the right solution?
When a coding agent gives you a solution to a problem, it implicitly biases you to that particular solution. It doesn’t mention all the other possible ways you could have solved the problem and the tradeoffs between them. It can also introduce design decisions that you might not catch on a review. Those decisions can lead to subtle bugs or limit what you can build in the future.
When I worked at Stripe Treasury, I was in a customer meeting where they were doing a money flow with ACH and needed a way to reconcile a money transfer with the user it was meant for. The obvious solution to this would have been to pass the id with the ACH in the memo field but it was too short to fit the id and the other needed metadata. So in the meeting we proposed a number of solutions to the problem involving new systems to handle that reconciliation process. However, I knew that under the hood the transfer existed as an OutboundPayment where the actual money transfer mechanism could be swapped. A wire had a much longer message length and would handle the id and metadata with ease. Switching the ACH to a wire solved the problem without requiring new systems for us to manage. That solution came from the intuitive understanding of the domain I built through living in the code and solving problems throughout the stack. The cost of letting AI think for you won’t necessarily be in the immediate PR but in the meeting where you aren’t able to connect dots that let you suggest elegant solutions.
Can’t code review solve all this?
The traditional contract in software development was that the writer of a PR took the time to create the design and write the code and so exerted most of the effort related to the code. The code reviewer was just another pair of eyes willing to provide feedback. If the code reviewer now has to understand the context of the feature you created well enough to point out every flaw in your design, they could have written the code in the first place. The more likely scenario is that your PRs will either get rejected outright, which reduces velocity, or rubberstamped, which degrades code quality.
The other answer is an AI code reviewer to manage the volume of PRs that get created when you can prompt with abandon. The issue is that an AI code reviewer doesn’t ask you if you are solving the right problems or if there is a better solution. Its remit is clean code, performance, security and other code-level concerns. It also holds no liability if bugs or design flaws get past it and turn into a production incident. That responsibility remains with the engineer. How does an engineer interpret the signals their system provides without an intuitive sense of how it works?
Does every task require this level of care?
Tasks like one-off scripts or documentation updates can be done fully autonomously with a review at the end because they don’t require meaningful design choices. The output just needs to be correct. That said, one-off scripts tend to become critical infrastructure without you noticing. So keep evaluating whether a task can fit into this fully autonomous mode.
A natural climb up the abstraction ladder
The obvious counter-argument to this is that coding agents follow a well-trodden path of abstracting complexity. Today you can easily go through your career without needing to know the details of how hardware works thanks to compilers, high-level languages like Python and technologies like the browser. However, previous abstractions were deterministic. They took common but complex sets of operations and papered over that complexity with a simpler operation. You might disagree with the choice of abstraction and its inherent tradeoffs, but if you do trust it, you can trust the output. You don’t need to check what your Python code is doing under the hood every time you run it.
LLMs, on the other hand, are probabilistic machines. Given a specific input and model, we can predict the range the output will fall in, but not the result itself. That is fundamentally different from a compiler. We can’t build on top of it without validating what it produces, and that validation requires understanding the context around it. Ironically, that’s the first thing you lose if you just accept the output.
The illusion of productivity
Watching code pile up is addictive because it feels like accumulating an asset. However, code is a liability. The asset is the product the code delivers. The complexity of your codebase, and by extension the likelihood of bugs, grows much faster than the code itself. That growth is mostly inevitable because successful software tends to add features. Even so, the goal should be to keep the complexity of the code roughly in line with the value it produces. If not, you might find that any change starts taking longer even with coding agents. You might also find yourself in an incident at 3 AM trying to decide whether you can trust an AI-created bug-fix, for AI-created code, in a system you don’t really understand without AI anymore.
Conclusion
Won’t AI become so good in a few years as to make all this moot? Perhaps. Five years ago I wouldn’t have predicted where AI would be today, and I don’t want to predict where it will be in another five years. What I can say is that even a perfect agent still needs someone to decide what problems to solve and how best to solve them. I want to make that person more efficient at working through those decisions instead of deciding for them.
I’ve worked with coding agents since the days when you had to review every line because they would get basic things wrong. I’ve always been frustrated by agents that tried to solve the problem for me, but I didn’t want to go back to working through every decision manually. Instead, I wanted an agent that discussed the problem with me, explored possible solutions and their tradeoffs, while leaving the final call to me. That’s what I built rubberduck to be. The act of evaluating design options against a set of criteria forces you to understand the system you are building, and that understanding is what prepares you for that customer meeting or 3 AM incident.