Assorted thoughts on constructively being bad at your job
I think it is very important that at some points in time, you are bad at your job. I think being bad at your job creates the necessary conditions for you to be good at your job. While LLMs can be a productivity boost when applied judiciously, I fear widespread adoption of LLMs hurts the experience of being bad at your job, and much like an ill-set broken bone, allows undesirable growth that will need correction later on.
Why you should start out bad at your job
When I think of how I got to where I am now, professionally and personally, I think it’s entirely self-evident that it is the sum of my experiences to date. Mistakes and successes alike have been piled on top of each other haphazardly, slowly forming what is… me. My first ever software job was as a student during my electrical engineering degree, at a lovely little startup called Sightline Innovation. On my first day, one of my about-to-be seniors named David walked me into a rather unremarkable office room with another 4 people, pointed out a desktop PC and said, “this is yours. We’ve installed Ubuntu for you, feel free to get set up with whatever terminal and desktop environment you’d like. I like zsh.” He turned and walked to his desk. I had never used a Linux OS before, and I certainly didn’t know what a zsh was. I was quite confident I would be setting a record for shortest employment term. What actually followed was among the best days of my career, filled with stupid questions, a complete lack of fundamental knowledge, and a group of people who were entirely smarter and more educated than I was.
The process of figuring out what zsh was, of asking long-suffering David about what I should do next, about how I should approach a task, and why he rejected my PR was an incredibly formative one. Some things I learned directly, such as “this is why we don’t hardcode magic numbers” and “string comparison is regularly brittle and undesired”. The most important things I learned, though, were not facts or rules of thumb. I believe the most important lesson I learned during this time was what the important questions were. I learned this by watching David ask me questions, by watching David interact with fellow seniors Tony and Jason. I learned that among the most important questions one can ask is the ever simple “context?”. What I really learned was that good software engineering is, by percentage, not about writing good code.
Good code is really a side effect of good design, and good design is really a side effect of asking good questions. Good design requires looking at the big picture from several perspectives, and trying to line those up into a coherent image. What does the user actually want? Does this provide them with actual value? What constraints are applied on us, either by business needs or by other systems? What are the consequences of this particular choice? Can we easily revisit this decision? Once you start asking these sorts of questions, you can start to sketch out a design. If you’ve done a good job, you’re almost forced into writing good code. Because good code is rarely at the line level, and very often at the interface layers between bits of code. Asking the right questions allows you to establish invariants and constraints, and those inform API and interface designs between systems, and everything past that is an implementation detail. There is some magic in a good implementation as well, I don’t intend to entirely skip over that, but the literal lines inside a function are typically less important than your interface, e.g. the function definition and how things call your function.
How do LLMs help us? How do they hurt us?
LLMs are very good at writing code, literally speaking. Language models are very good at pattern matching in languages, and code is a form of language which features a very high number of strongly defined patterns. Naturally they’re quite adept at interpreting and producing things like switch/case statements, etc. This would fit into the “implementation detail” portion of software engineering, which as I’ve mentioned, I think is not the most important or complex part of any software project. Writing those implementations are something we should be able to do, but ultimately are “direct knowledge” learning that doesn’t really improve your career that much. I don’t see any downsides in learning what well-written code is and then outsourcing it to a spicy code generator.
I don’t necessarily think that applies to good design, though. Time and time again, claude or copilot catch simple syntax errors while completely misunderstanding the semantics behind what a function should be doing within the context of a bigger system, and they very frequently produce systems with overwhelming complexity and very fragile system interactions. I’ve found claude is very readily able to generate a 100 line function with ease, but if you ask it to “update all uses of this function for the new signature”, it frequently will miss several invocations or get some of them wrong entirely. Even worse, if you ask it to design a new subsystem to implement a new feature within an existing system, there’s a very good chance it will produce twice the code it needs to with much more complex interfaces and intended usages. I think it’s because claude doesn’t know to ask those good questions, and even if it does ask for input during the planning process, the technical limitations of context windows and attention modules prevent it from applying those questions and answers consistently and semantically accurately.
I think the worst performance I typically see out of claude is when I ask it to refactor similar code into a reusable component, and then update the uses. The interfaces it cooks up are not by themselves nonsensical, but it rapidly loses the overall plot and gets fixated on one particular interface detail. That detail then becomes its primary mission, and the resultant refactor is an overly complicated web of classes and interfaces where half of them aren’t needed. If I use LLM assistants for these tasks, I often find I can cut the LOC in half either manually, or by simply selecting snippets of code and asking “is this redundant?” or “can we simplify this by doing foo instead of bar?”.
Claude is an intellectual terrorist
When you reach a certain level of experience, you know what a good design looks like because you’ve learned about those good questions. When Claude recommends a dumb path, or writes nonsense code, you can smell it. You have the intuition and experience to know why this path is bad within the greater context of the project, and you also know how to nudge bad designs back on track. When you’re a junior who doesn’t have that intuition, claude is your confident university friend who cheated on every assignment who hands you something that looks approximately correct and tells you to just trust it. You don’t know what overcomplicated looks like, you don’t know what questions to ask to simplify the problem, and you might not realize you’re making a one way door decision… so you accept the change, commit the code and put up a PR. Some senior reads it and because you’re somewhat close enough, assumes you’ve gotten the appropriate context and understand the prerequisite details of the overall system, and leaves you some comments about how to clean up the implementation. You iterate with claude and PR comments a few times, eventually its good enough (or the senior is impatient enough), and your PR is accepted. Ticket done, great!
Crucially though you’ve missed out on some key parts of being a junior, namely learning why those nudges were being made, learning why it’s overcomplicated, and ultimately understanding how to just not put up an over engineered PR in the first place. You also have a very high likelihood of needing to deeply refactor that code later, at least in my experience. By the time you write it correctly for the business requirements, you’ve sufficiently decoupled the original mistake from the correction, which will significantly weaken your ability to learn from it. I used to hate when David would tell me to re-write sections of code because it wasn’t quite what we wanted, but I think getting that feedback at PR time was essential to me learning how to do it correctly.
And that’s nothing to say of the senior’s time, which is why I consider claude to be an intellectual terrorist. With claude, a junior can put up 2-3 times the amount of fundamentally incorrect code in the same time frame. PR sizes are bigger, and the number of review iterations before merging is higher. My inbox is now an intellectual DDoS by PR, where my comments are only ever partially implemented in the next iteration because the junior never really understood the reason behind the comment, and simply asked claude to guess at what I meant. And I think that’s the ultimate shame of it: we’re robbing our juniors of the opportunity to be good at being bad at their jobs. My juniors are human wrappers around claude, some of them even at the point where instead of trying to understand the context of good design, they simply copy/paste claude’s plan output to me, copy/paste my responses back, and then put up a claude-written PR for me to review.
You can say that they’re just bad juniors, but are they? They’re doing what management wants them to do. They’re moving 2-3x faster. Their PR metrics are way up. They’re able to muddle their way through more complex projects than they would have been able to alone. And worse than that, as a senior I know claude’s made me lazier, too. Can I really blame them for wanting to take the same shortcuts that I’m able to, not knowing that I’m only able to because I can understand why claude has made a mistake?
so… what?
I don’t know. LLMs are a very useful tool in the right hands, no one can debate that a judiciously used LLM can save time on less-than-very-complex work. They’re certainly not disappearing. But we also need to not disappear our juniors, and we need to encourage them to engage intellectually, not just push out code. LLMs encourage you to turn your brain off and vibe, and that’s being bad at your job badly. We need them to be good at being bad at their job, because if we outsource being a junior to an LLM, we’ll stop getting seniors. And seniors are propping up the whole LLM coding scheme via reviews, via good initial designs, and by systems-level thinking. You can’t get a good code architect without having some bad juniors first, and I worry we’re not giving them space to be bad in the right way.
But what do I know? I’m a young old man yelling at cloud software. Maybe David saw me the same way, as some lost junior who isn’t levelling up quickly enough and who isn’t learning fundamental lessons. Maybe I’m the teacher in high school saying you need to learn math because we won’t always have a calculator around. Or maybe we really are watching the beginning of the end of seniors as we outsource educational opportunities to an LLM.
If you’re a junior, be bad at your job correctly. Your senior would (should?) rather take an hour up front to walk through things than 3 hours later with a PR that’s many thousands of lines long. Learn. Ask the right questions.
If you’re a senior, godspeed. May your review request queue be short and your PR size appropriate, and try not to let your juniors intellectually disengage from the projects. Make space for them to be good at being bad. Don’t let them overcomplicate things, and ensure that they actually understand things before you let them go back to claude.
Anthropic to the rescue?
I’ve been sitting on this blog post for a few months now, and finally decided to post it after seeing this pre-print (linkie) from some folks from Anthropic.
We find that AI use impairs conceptual understanding, code reading, and debugging abilities
…
Our findings suggest that AI-enhanced productivity is not a shortcut to competence and AI assistance should be carefully adopted into workflows to preserve skill formation
I won’t claim that this is any sort of proof of my correctness, but there is certainly some level of overlap.