Introduction
The virtue of formal texts is that their manipulations, in order to be legitimate, need to satisfy only a few simple rules; they are, when you come to think of it, an amazingly effective tool for ruling out all sorts of nonsense that, when we use our native tongues, are almost impossible to avoid.
E.W. Dijkstra – On the Foolishness of Natural Language Programming
In the previous part I went over the history of “coding is dead” movements and why, on a conceptual level, any attempt to remove or reduce coding from computer software is confused and doomed to fail.
In this part, I want to really dig into what using a “coding assistant”, or delegating the coding part of programming, does or even could really look like, and why it is, too, a confused concept that emerges from the confused base of coding being something that even could be eliminated or reduced from software development. This is, of course, about “AI Programming”, but that is just a specific manifestation of the general idea. Recall that I believe AI is computerized charlatanism, but even if I believed AI was closer to what it promises to be (a “genie in a laptop”), it wouldn’t change the fact that using it to “do the coding part for you” is a nonsensical idea.
Either you are the programmer (software engineer), and then it’s your job to carefully author code, or this software engineering role has been entirely delegated to the genie in the laptop, and then you are not an “AI Programmer”, you’re a product owner in an organization whose engineering department is a bunch of clankers. The latter is exactly what your product owners, managers and especially executives think is either already happening or just a few months away (after AI gets good enough and/or you finish building your robot replacement and then can be fired), but “AI Programmers” obviously don’t think their entire career prospects have gone up in smoke. They think their future is in this new “agentic engineering” role where they are still needed to do the software engineering part, but they just hand the coding part off to a sort of automated script. This is a schizophrenic concept. Sort of like “we can now create general purpose software without code” is “trying to have your cake and eat it too”, this idea of “AI programming” is a self-contradicting “foot both in and out the door” idea. Are you still a software engineer or not?
To be clear, since I believe generative AI is computerized charlatanism, I cannot even seriously entertain the idea that it will ever become a software engineer itself, so I’ve never even been slightly “worried” this is going to happen (what’s to “worry” about anyways, that we’re about to live in Star Trek replicator heaven?). Nothing that the software industry has wasted its time with in 2025 or 2026 even slightly suggests this is happening. The Rube Goldberg machines of “agentic software development” are rather desperate attempts to cancel out the bullsh*tting nature of token transformers and get them to act as some kind of generic scripting language, albeit one where you have to repeatedly explain to (as some people would describe it, beg) it to follow instructions. If you actually look at what programmers are doing here, you’ll see they’re writing tons of automated tests to try to “steer” the “agents” toward producing code that satisfies the requirements. You’ll see graphics illustrate this, e.g.:

This, amusingly, illustrates that AI literally made no difference. The length of the entire pipeline didn’t change at all, it just shifted the most time spent from writing mostly correct code to begging the chatbot to please write correct code (“context engineering”) and finding all the problems with the code it didn’t write correctly (“testing and verification”, which exists to begin with because there are mistakes in the execution of previous steps). This was probably not the intention of whoever created this graphic, but that makes it even funnier that it accidentally makes that point.
When you look at graphics like this, what you see is not people trying to make software development more efficient or productive, but rather people who work in software development but hate coding doing everything they can to minimize how much of their day-to-day job involves coding (this is very important and the subject of the next part of the series).
This kind of stuff is, indeed, nothing like AI being a genie in a laptop that can just take your request to build a software system and do all the engineering involved to build it. It’s engineers still doing the engineering but adding a bunch of complicated steps to the process.

This isn’t “coding is dead”. It’s still programmers typing instructions into their laptops all day, they’re just typing different instructions in different formats and languages. Coding is not dead, it is different, and so we need to have a conversation that I find very odd no one seems to have been having for the last year: is this new form of code actually better that the old form of code, and why?
Natural Language Is the Worst Choice for Code
What, in broad terms, is the difference between “AI/agentic” programming and “manual” programming? Referring to the “old fashioned” process as “manual” is already misleading, as if there is any less “on hands” process with AI than before. Like I said, programmers are still typing instructions on their keyboards into programs running on their development laptops just like they were before. This is no less “manual” than typing “code” directly was. Automation doesn’t necessarily make things less manual. We can imagine a machine that “automatically” performs a task, but the number of “manual” input steps required to prepare the machine to perform the task is actually greater than the number of “manual” steps needed to perform the task directly. The machine isn’t making it any easier, it’s just adding an unnecessary complication to the process.
The primary, though not only, difference, comes down to the language: “old fashioned” programming is writing instructions in a programming language like C, Java, C#, TypeScript, etc. AI/agentic programming is writing instructions in plain English, in the form of prompts and various additional artifacts that are consumed by coding harnesses that eventually become parts of prompts (AGENTS.md, skill files, spec files, etc.). These all have no structure except the rudimentary formatting provided by the markdown format. Unstructured English is, indeed, very different as a language for describing precise instructions for software than industry programming languages…
…and in every possible way it is worse for this task. In fact plain, unstructured natural language is just about as bad of a choice for writing precise instructions as you could possibly make. It is worse than machine code.
To understand why, you can start by reading the above-quoted piece by Dijkstra, On the Foolishness of Natural Language Programming. Dijkstra wrote this in 1978! So, once again, nothing is new under the sun, and if anyone thinks “we can finally make software by just talking in English!” is a new concept that has not been analyzed to death by computer scientists, they only confirm their ignorance of history.
Following an analogy he presents, imagine if we applied this same reasoning to algebra. Finally we can do algebra without using symbols! No + or =, no vertical stacks separated by bars (fractions), no parentheses, and certainly not letters like x to denote unknown values, instead we just describe equations in plain English! Doesn’t that sound great!?
Uhh… NO!!! No it does not sound great. It sounds downright awful, actually. Algebra would be nearly impossible without the symbols created specifically to efficiently and concisely express algebraic concepts. It’s not like these symbols were forced on us. Humans invented them specifically to avoid the tedium of talking about such things in natural language.
The big problem here is everyone outside the engineering department (who otherwise loves to talk about the technical details of computer programming), and a depressingly high number of people inside the engineering department, have no understanding of why programming languages work the way they do. They think programming languages are some kind of compromise we made with computers to converse in a way that’s kind-of nice for both us and the computer but not ideal for either. So they think finally unlocking the ability to talk to computers (including for the purpose of describing software to them) in our native languages is this big win that we’ve simply not had sufficient technology to achieve until now.
This is completely confused and backward in almost every way.
Parsing natural language is not some cognitively intractable problem that takes black box machine learning “don’t try to work out the algorithm, just tune it to a huge set of precomputed correct answers” techniques to crack. It’s not like computer vision (trying to reduce the construction of 3D scenery from pixels is such a problem, no one’s been able to crack the algorithm and they managed to sidestep this by just tuning a generic input/output function with known correct answers, a.k.a. “deep learning”). Languages have precise, well-defined grammars and dictionaries. This was problem was solved algorithmically, no “AI” required, decades ago. It’s how Microsoft Word was able to flag spelling mistakes in your document since the 80s, and grammatical mistakes since the 90s.
The problem is not syntactical but semantic, and this is not a technology problem, it is a problem with language itself. As I briefly discussed in the last part, natural language does not just express instructions. It expresses declarations about the world, emotions, wishes, predictions, questions, etc., and it supports vagueness, which it must, because thoughts, declarations, predictions, etc. are often vague and we need a way to communicate them faithfully. Natural language does much, much more than provide a way to express precise instructions, and the line between language that provides precise instructions and either provides vague, non-actionable instructions (clarification is required to make it actionable) or no instructions at all (it’s a poem or something) is very blurry and complicated to delineate.
A correctly functioning “compiler” or “interpreter” for natural language would reject anything that is not fully exact and exhaustive instructions as erroneous input, the same way a C compiler rejects a call to a function that doesn’t exist. The point is that we already built compilers that do this! The first major difference between high level programming languages and natural language is that they are highly constrained. The vast majority of valid English sentences are not valid Java statements. Syntactic minutiae like separating tokens by dots instead of spaces are irrelevant. The point is that the grammar of a language like Java is the subset of English that restricts it to exact and exhaustive instructions, rejecting anything else (declarations about the world, questions, exclamations, vague instructions, non-actionable instructions, instructions a computer system would not be able to perform, etc.) as syntactically invalid.
We’ve already done the work to carve out the subset of natural language that is valid for describing computer programs. They’re called programming languages.
Another type of input that a compiler/interpreter needs to reject as invalid input is instructions that violate the stated rules of the software system being built. These rules aren’t part of what needs to be executed by the program itself, they are rather a constraint on what executions the program should do. These rules can be rather sophisticated by themselves and need to, to some degree, be computed, making them a sort of metaprogram that needs to be executed whenever the actual program’s instructions are verified. These rules get intricately intertwined with the program’s actual instructions, so they need to both be referenced in the same documents (you can’t draw the distinction on a file/document level), which requires the distinction between them to be annotated with special syntax throughout the “source” documents. These annotations allow the program’s instructions to be “decorated” with information that is not an instruction per se (not one that should be performed when the program is running) and picked out by a static analysis tool.
The only way to make this type of analysis possible is to annotate the instructions in ways that indicate certain rules are known at design time. These annotations are added to programming languages to make static analysis (analysis by inspecting the source code instead of executing the instructions therein) possible, specifically a type of static analysis that the programmer can add his own rules to. The primary forms of this are function signatures (which indicate a certain symbol is a function, i.e. something that can be invoked, and usually indicate at least the number of arguments the function can be invoked with) and types, which ultimately determine what syntactically valid statements are semantically valid in the context of a program.
I have met a lot of programmers who think static typing is some kind of verbose burden dumped on programmers to be redundant in the instructions they write in code. Their favorite languages are ones like Ruby, which is explicitly designed to be as close to natural language as possible. This is totally confused. They don’t understand that the extra “fluff” in a language like Java or C# compared to Ruby is exactly why we have “Refactor -> Rename” tools in IDEs that work perfectly with absolutely no errors whatsoever in those languages, while if a comparable tool exists at all for a Ruby codebase, it’s guesswork that can’t formally distinguish calls to a specific method in a specific scope named doX from other callable symbols that happened to be named doX, including local or class scoped variables or an identically named method in another scope. Correspondingly, that refactor tool almost always does the wrong thing, and it is not possible to write a tool that does this operation correctly in Ruby.
Non-programmers and confused programmers will see all this “extra” stuff in code that wouldn’t be there, at least not as repetitively, if you were just talking in plain English, and think this is some kind of defect in such programming languages, or even worse something put in as a concession to make it easier for computers to understand it, at the expense of burdening programmers with more semantically empty “ceremony”. The latter is obviously not true because we have multiple programming languages like Ruby and Bash that don’t have these annotations. So computers being able to interpret such languages isn’t a problem. The requirement for such annotations in other languages is a deliberate design choice for those languages. We choose to make them work that way, for reasons that have nothing to do with helping computers understand them.
In fact, many such aspects of strongly and statically typed programming languages make writing the parser/compiler for them harder. In language design communities, people will often propose features for a programming language that enrich and complicate the rules for its type system or other annotations, that are pitched on the basis that they do something for programmers, they allow them to more robustly express rules that the compiler will then enforce at compile time (thus allowing larger classes of bugs to be rejected at compile time, preventing them from ever getting to even testers, let alone customers), but then get rejected because it’s too difficult to implement them in the compiler! That is, these language features make computers parsing and understanding the language harder. They aren’t compromises that push the language away from human-serving toward computer-serving.
Modern high-level programming languages have incredibly complicated compilers to make them work. These are some of the most complex and cognitively demanding programs that exist, far beyond the capabilities of most programmers (they would not be able to understand at all what the source code for these compilers are doing). We bent over backwards and put all this time and effort into building these compilers in order to serve programmers modern high-level programming languages. They have been designed for half a century specifically to serve the needs of programming, and are excellent at doing so. Anyone trying to avoid programming languages for writing software is like someone trying to avoid a car if they need to travel 30 miles down a road, and talking about how “I just traveled down the road without ever touching a steering wheel!” is some kind of win. What are you guys talking about!? These are the state-of-the-art tools made specifically for this problem!
This is not restricted to programming either, not by a long shot. Lawyers create restricted forms of natural language with which to write legal contracts (“legal jargon”). Every academic discipline invents terminology and often symbology (“academic jargon”) to efficiently and precisely express their ideas. No one talks about getting rid of this, so why are so many people talking about getting rid of programming languages in software?
AI is a Higher Level of Abstraction!
Using natural language to describe software is throwing away 60 years of progress in the fields of computer science, type theory, lambda calculus, formal grammar, and probably others I’m not thinking about. But curiously, AI Programmers claim exactly the opposite: that using AI is somehow just the next iteration in programming languages that moves us further away from machine code, in the same direction that existing programming languages already took us. They imagine a topology like this:
Machine Code -> Assembly -> BASIC/C -> Java/C#/TypeScript -> English
But everything I explained above indicates this makes no sense. There are of course many different programming languages in use today. There are OOP languages like Java/C#/TypeScript, but also Ruby and Python, which have always been more appealing to non-programmers (Python especially became the de facto standard for people who don’t want to get too much into programming to nevertheless write some programs). As I mentioned, the main design goal of Ruby is to be as close to natural language as possible (correspondingly its creator has successfully thwarted every attempt to add static typing to it, to the point it is now the last remaining industry language with no static typing, which PHP, JavaScript via TypeScript, and even Python now all have).
“AI is at a higher level of abstraction” seems to be a buzz phrase that probably originated from AI lab sales engineers marketing chatbots to tech executives. They know programmers talk about “abstraction” as being a good thing, so calling AI “the next level of abstraction” might convince a lot of programmers it’s the future of programming. I say this because I have not detected much real meaning in this phrase. I haven’t seen any concrete examples or elaborations on what this means, exactly. It seems to function primarily as a thought-terminating cliche.
It’s already questionable that OOP languages like Java/C# are at a “higher level of abstraction” than BASIC/C. C already has an essentially unlimited ability to raise the abstraction level. Abstraction means interfaces that express what to do without explaining how to do something. Any programming language that has functions can be used to build abstractions all the way up to the level of business specifications. This is possible in all of these languages, including C. The abstraction ceiling is not the difference between C and Java/C#. The difference is rather in what kind of conversation the programmer can have with the compiler, especially around design time constraints. This has more to do with verification of the program’s correctness, specifically at design (compile) time, than with building layers of abstraction that allow the business specifications to be written directly.
For example, Java/C# has classes that support inheritance, including abstract classes. You can build class hierarchies in C, and you can achieve the same level of conciseness (you don’t have to do it as a design pattern with boilerplate code templates) as long as you are willing to “build” the classes (lay out a struct with a pointer to a vtable that dispatches function calls through it) entirely at runtime and call their methods through strings and variadic parameters. You can support the concept of an abstract class that requires inheritors to override certain functions, and this will be enforced when you call the function to define a subclass. It will check if the base class is abstract, and if so, which functions need to be overridden, and if the subclass overrides them, which if it does not, will trigger an error.
The problem is this is all at runtime. There are performance problems with this (you’re spending CPU cycles and memory constructing all of this every time the program runs, even though it’s the same every time), but more importantly any mistakes, like creating circular class hierarchies, declaring overrides of functions that don’t exist in the base class or that aren’t marked as overridable, not overriding abstract functions, erroneous casting (like “sideways” casting across unrelated hierarchies, which is known statically to fail), or even calling non-existent methods, will all be runtime errors. They will be errors that occur while the program runs (triggering an abort or setting an error code that needs to be checked, etc.) and only when a line of code is actually encountered by the executing program. You built a class system with inheritance that the C compiler doesn’t understand. From its perspective it’s just a bunch of data structures and algorithms. So it can’t help you enforce that you’re following its rules.
If you do this in Java/C# with the tools the language gives you, the compiler does see what you are doing. The compiler understands the class, extends/inherits (or just : in the right position), overrides, virtual/final and abstract keywords. It recognizes those as special keywords with specific meaning, and it is able to check and enforce rules for using them correctly at compile time. If you make a mistake like circular inheritance, overriding something nonvirtual/final, or not overriding all the abstract methods of an abstract class, or casting across unrelated hierarchies, or calling a nonexistent method, the compiler flags it immediately and won’t even let you build your program until you fix it.
This isn’t about a missing ability to build as many abstractions as you want. Different languages have different levels of ability, and different techniques, to build concepts and rules at compile time instead of at runtime. It is the enhancement of the conversation you, the programmer, are able to have with the compiler (the thing that parses the language you’re writing in), that distinguishes different high-level languages (BASIC and C are both high-level) from each other.
And guess what natural language, including the prompts and markdown files AI programmers write, doesn’t have at all! That’s right: compilation, or any kind of static analysis pass that scans it all and detects errors. Even scripting languages do this when you first run them. The JavaScript interpreter will at least scan the entire script file immediately and reject it if there is a syntax error. You don’t even get this with the markdown files. You get no static analysis at all! And you can’t beyond basic “this isn’t even valid English grammar” or “this isn’t a real word”, because what enables static analysis is precisely the stuff that makes programming languages different than natural language. It’s those annoying syntax rules and annotations that enable static analysis that the people excited to never look at “code” again specifically want to get rid of.
So in the above sequence, English really belongs next to machine code. It’s a language with absolutely no static analysis whatsoever beyond basic syntax. Even assemblers will reject assembly that contains an invalid/unrecognized instruction or tries to CALL a nonexistent block. The total lack of static analysis that you just have to YOLO by executing it and hoping for the best is raw machine code (it’s just a bunch of bytes, every byte means something to the CPU so it’s going to do whatever that happens to be).
AI Is a Higher Level of Ambiguity
What I think people are vaguely trying to get at when they say “AI is the next level of abstraction” is this idea that with AI, now programmers can basically just be technical product owners. They can now just describe the business requirements/specifications in their prompts and markdown files, and the AI will act as a “library” that “implements” those requirements, the way programmers used to have to do themselves. This idea completely misconstrues why business requirements in JIRA tickets have never constituted the source code of computer software. It’s not because they’re “too high-level” or “too abstract”.
Plain English is not a higher level of abstraction, it is a higher level of ambiguity
An in the discussions I have witnessed, it is clear that a lot of programmers don’t understand the difference between abstraction and ambiguity, which is very unfortunate. “Abstraction” really is a buzzword a lot of programmers have been trained to blurt out whenever they want to defend something as good.
When you write array.sort() instead of writing all the lines that implement a sorting algorithm, you are not introducing any ambiguity into the program. It is still precise what is going to happen, and what the final state of the array is going to be. It is more abstract to simply say “sort” instead of explain how to sort. It is not ambiguous.
What people call “business requirements”, the acceptance criteria, etc., are almost always highly ambiguous. They do not explain exactly what the software should do. They leave out a lot of details… not implementation details (which product owners usually erroneously call them), but user-facing details that are part of what the software does, not how it does it. Product owners often use “implementation details” to mean “things I don’t care about”, but those are different. “Whatever, you pick something” is not the same as “do this, exactly this, but I don’t care how“.
If you try to put those vague, ambiguous requirements that are missing tons of critical details into source code files and hit the Build button, it will fail, the compiler will reject it as erroneous input, even if you put it in proper syntax for the language. But if you feed this into a chatbot prompt, it won’t say, “bro, there are so many missing details in this, you need to tell me exactly what you want for X, Y, Z, … before I can do this”, it will say, “sure thing, boss. I’m on it!” and then spit out… something, possibly something that you can then build and run.
If you think that is a good thing and encompasses the awesome power of AI, you could not be more wrong! It is not a good thing that chatbots invent missing requirements (and in fact requirements that weren’t missing, it’s just something the chatbot thought you’d like to be added) instead of tell you that requirements are missing and you need to fill them in. Being able to invent requirements if you ask it to is one thing. Inventing them automatically is another thing entirely. And if a chatbot ever takes vague, ambiguous business requirements in plain English and emits computer code from it, it is already guaranteed that it invented requirements not in those prompts because the input was vague, ambiguous and missing details, but the output is a computer program that runs and does something exact and fully detailed. The gap had to be filled in somehow (the way generative AI works is by filling in the gaps with whatever hopefully relevant examples were on the internet somewhere, i.e. filling in all the details for the layout and color template for websites, that aren’t in the prompts, with the default Tailwind black/purple style that every AI generated website uses).
Yes, human programmers can and frequently do this too. They take the vague, ambiguous requirements from product owners and fill in the gaps themselves with whatever they think the missing details should be. That’s almost always not good, and even when it is, it’s something you want a human to reason through, not a procedural generator to randomly grab examples off the internet for. We have put considerable effort into software development processes to catch and prevent this kind of requirements invention as early as possible, and force a feedback loop to bring in the stakeholders to properly address it. This is a large part of what BDD is intended to do: by having the stakeholder, engineer and tester collaborate (like literally be in a room together, it can be a virtual room) on authoring the specification, these missing details are shaken out early on, before any work begins that, if done based on misunderstandings, will lead to churn and waste, and placed properly in the hands of the stakeholder to be the decision maker (who can then delegate it if desired).
Finding problems, not least of which are missing or unclear details, in specifications for software as early as possible is a major part of what the software industry has been discussing for decades, all the way back to the original Royce Paper whose intentionally bad example of how not to run a software DLC was straw manned by Agile Inc. into “waterfall” (something they claim the software industry widely practiced before they showed up and saved the day). This counterexample was presented specifically because it lacked internal feedback loops that would detect mistakes and reverse the flow as soon as they were detected. The lack of such feedbacks meant that mistakes would propagate all the way toward final testing of a completed system, at which point they would be noticed and then they would have to be fixed, which is enormously expensive not just because of all the time wasted building the wrong thing, but because fixing problems like that is much harder in a finished system (the mistaken behavior may have become a “load-bearing column” of the overall architecture that can’t simply be removed or modified without significantly redesigning large parts of the system) than in a design phase.
Either way, even if we wanted to dump the responsibility of being the actual product owner (filling in all these requirements details) onto a chatbot, this has absolutely nothing to do with the chatbot writing code for you. It has nothing to do with prompts and markdown files being a better or more efficient language from programmers to work in. This would mean programmers feed the requirements they get from JIRA tickets into a prompt with the instruction to fill in the gaps and emit a better requirement, which the programmer would then formalize the syntax of to make it code, which should be far less work than explaining it to a chatbot and confirming it did so correctly. If there’s a large gap between requirements and code, the programmers need to build more abstractions.
You Don’t Have to Worry about Design Anymore!
Another idea is that delegating the “coding” takes the burden of “architecture” or “design” (as in code design, not graphic design) off of programmers and lets them “spend more time thinking about the problem”. I have heard ludicrous statements ranging from “now you don’t have to think about the right way to code a behavior” to “now you don’t have to waste time thinking about what to name every variable” in regard to this. I don’t know how people expect to be able to refer to a specific object in a system in the prompts in multiple places except by giving it a name, so I don’t know why they think prompting instead of “coding” somehow avoids the problem of thinking of names for things.
This one makes me the most upset of all, because this notion that architecture/design in code is some irrelevant artifact of programming languages or some other aspect of programming that any kind of “higher abstraction” tool can “unburden” us from is the quintessence of programmer incompetence. Like all of this, this is also not a new concept, and it’s one I’ve been fighting as hard as I can since I realized code design (or code “quality”) is everything a couple of years into my career.
Once again, this is exactly backward. How much “design” is there in machine code, in the sense of choices over how exactly to structure or lay out the code paths while keeping the user facing behavior the same? Well not none. For example, you can choose whether to code something as a loop (a jump instruction back to the start of a repeating section) or unroll it. You can inline sections into copies or write one version as a section that is jumped into and out of. You have to decide what algorithms and data structures to use. But overall… not much. How many books about “design patterns” are there for machine code, or assembly?
No, design or architecture is an aspect of programming that becomes increasingly important as we move away from machine code into high level languages, including from procedural languages like C to OOP languages. Design patterns are mostly associated with OOP, and mocking design patterns as excessive “over-engineering” is mostly aimed at Java. Java programmers are more burdened with how to design type/class systems (choosing composition or inheritance, for example), where to draw abstraction boundaries, how to make parts composable, and how to create only appropriate coupling and avoid inappropriate coupling, which is especially concerned with access scope (public, private, etc.), which are concepts introduced into high level languages that don’t mean anything once the program is compiled and running (from the machine or OS’s perspective, every part of a program has unfettered access to all parts of its memory). Ironically, this is frequently submitted as a major mark against OOP languages by C programmers, who think design patterns are just wasting time overcomplicating programs.
The idea that delegating coding to a chatbot (or anything else for that matter) suddenly makes such concerns not matter is a non sequitur unless you believe all of those concerns existed solely to make programmers reading code happy. This is a popular idea, as if there is something peculiar about humans that make design patterns important. The implication is that code design is some kind of aesthetic or emotional issue, which I will discuss in more detail in the next part of this series. But that is not why they emerged, and (this is the main subject of the Gang of Four “Design Patterns” book) spontaneously (through repeated independent reinvention) converged onto a standard set of ideas and techniques.
The purpose of design patterns in code is to organize and manage complexity.
Modern software is extremely complex, not in its “implementation details” but in its requirements or precise specifications. The ironic thing about claiming “programmers now get to be technical product owners” is (following the same pattern as all of this) that was always what they were! The lead engineers especially have always had the job of taking the vague specifications that only describe working software in people’s imaginations and adding all the details necessary to make it working software in reality. Again, this is not about “implementation details” (like choosing an array or a linked list, although even this has implications for user facing behavior through performance characteristics, which is the whole point of making a choice), it is about user facing details like how exactly a user interface is laid out and functions, what details (visible in the interface and often editable) exist in records and what rules they obey, and so on… things product owners rarely think of, but (at least the ones who actually do their jobs) once the lead engineer points out, they realize is missing details of the specification they need to decide on, or explicitly declare they don’t care about and let the engineer decide.
Here’s the thing that happens once all those precise, tiny details for all the intricate requirements get filled in: they become overwhelmingly complicated. All these specifications became an intractable pile of spaghetti that can’t be effectively incremented on (the key feature of software, which is rapid iteration or incremental modification) unless and until someone uses techniques to design the way those specifications are defined and expressed in order to keep them well organized and the complexity managed through abstraction/encapsulation (keeping complexity hidden from contexts where it isn’t important). This is what “code design” or “architecture” is and has always been about.
The tools to organize and manage that complexity, in ways that get statically analyzed and verified for internal consistency and conformance with constraints of the system being built, are a large part of what high level programming languages are designed to facilitate. When you take all of that away and try to specify software in English and not think about design concerns, you will get quickly overwhelmed with just how intricate and rich in complex features modern software is. If product owners think the well-designed and architected code expressing, exhaustively and precisely, all their requirements, is too much of a headache, just show them what the same expressed as a bunch of unorganized English prose looks like!
I’ve seen this exact problem come up multiple times when doing BDD and requirements analysis on enterprise software. Funny enough, the way product owners (remember that’s what we’re all supposed to be now, right?) deal with the overwhelming complexity of their business requirements is to literally avoid it by never writing it all down anywhere that even claims to be an organized, searchable source of truth for the software. Instead, most software these days is completely undocumented (except for the code itself), with the closest thing coming to an official ledger for its specifications being an unorganized history of JIRA tickets that would have to be collected and merged in order (since an older ticket can be superseded or even invalidated by a newer one) to be able to know what is the software supposed to do as of right now? By avoiding ever trying to build the official repository of requirements at all, product owners get to tell themselves that repository, if it were to exist, would be an easy read totally within their capabilities.
And then someone like me comes in and starts pushing them to actually do this by practicing BDD and requirements analysis, and almost every time, the reaction from product owners is to revolt against it, and specifically claim after taking one look at a proper set of specifications for even a small part of a system that this is code and not what they signed up for. In other words, they wan’t nothing to do with the full precise specifications for the product they “own”. I’m talking about Gherkin here, a sequence of plain English steps describing the initial conditions, action and final conditions of a single behavior in a system, no implementation details or algorithms anywhere to be found. That is what makes product owners throw their hands up and insist it’s not their job to worry about details on that level.
But even more interestingly, writing out Gherkin requirements for large systems becomes very tedious and hard to reason through because of the lack of design tools that full-blown programming languages give us. I realized after doing this a few times that “design patterns” are what we invoke to deal with this complexity, and that those product owners revolting against this sort of have a point: this isn’t their wheelhouse, it’s mine (software engineers’). What that means is: software engineers have always been primarily dealing with managing and expressing requirements. Especially in enterprise software where performance or other aspects that are sensitive to “how computers work” are lower in priority than getting all the intricate business logic correct, thinking about the requirements and how to express them in a way that keeps the complexity under control is already what we spend almost all our time doing, and it’s been that way for decades.
“Think about the problem” more? That’s how it’s always been… except for programmers who aren’t skilled in abstraction and design patterns so they really do waste most of their day writing repetitive boilerplate… but AI or some other “coding assistant” isn’t how you fix that, and in fact the last thing you’d ever want to do is hand novice programmers who work that way a tool that lets them generate this high entropy (little information for a lot of characters) code faster.
AI is the Next Compiler!
Somewhat related to the idea that “AI is a higher level of abstraction” that just continues the march from machine code to assembly to C to OOP languages, etc., there is this popular retort that “AI is the next compiler”, which is another way of saying natural language is the next programming language after modern industry languages. I have already addressed why this is completely backward. But comparing the AI coding workflow to a compiler specifically reveals some more major misunderstands of how the programming tools we use work, and why.
Besides working in a different language, what is the major difference between the way AI coders work, and the way “old fashioned” coders work? The answer emerges from the fact code is still the source of truth. The prompts and markdown files are not the official specification for the software system. The prompts are transient (they don’t even, generally, get saved anywhere, they go into a chat session, then that chat session closes and they are gone forever… and saving them wouldn’t be much help anyways) and the markdown files checked into the repo are, as far as the software system itself is concerned, irrelevant fluff. You can change those in any way you want, and it won’t make even a tiny difference to what the software does. If you want to change the software, you still have to modify the source code, which is still checked into the repo. But AI coders don’t write (and unfortunately for the ones suffering the most from AI psychosis, don’t even read) this source code. They only work on these transient and/or non-official documents.
There is no analogy between this and compilers.
There was never a point in history, however briefly, where programmers used compilers to write instructions, in a high-level language, for a modification to make to a software system, that is specified in machine code binaries, that would be executed by the compiler (the program that understands and executes the high-level language instructions) by reading in that machine code, performing the requested modifications to it, then writing it back, and then committing that modified machine code back to the repo. First, this wouldn’t work. The instructions to modify the machine code would have to reference things in the machine code (like variables, blocks of code, etc.), but those things have no representation in the high-level language, so how would they be referenced? Second, it would completely negate the value proposition of using high-level languages to begin with, which is precisely that they make the source of truth for the software system the high-level code.
Compilers shifted the code programmers work on entirely away from machine code to the high-level language. No version of the machine code needs to be maintained at all, because it is derivable from nothing but the high-level source code. The high-level code is not a series of instructions for modifications, it is the software, in its current form. Modifications to the system are made by modifying this code. The diffs from old to new can be reviewed, and tell a comprehensible story about what is being modified. If programmers ever tried to work in a way where they maintained a machine code binary, issued instructions on how to modify it, and didn’t even try to look at how the binary actually got modified and make sense of it (which, if they did, would mean they’d have to reason about machine code), they would lose control of the system they are working on, which might look fine until one day their modification tool stops working, and they realize they have a big pile of machine code no one wrote or has attempted to read or understand and suddenly try to comprehend it.
This would be a disaster that would end a software system abruptly. It would be literally unmaintainable. AI coders know this and defend what they’re doing on the completely baseless claim that AI will just always correctly modify the source code, with 100% reliability. They have no reason to believe this and submit no arguments for why this would be true, it’s just hand-wavy “but AI is AI, and it just keeps getting better!”, so then it must just do everything correctly, every time, or if it doesn’t, it will soon. In the entire lifecycle of a software system (which is supposed to be indefinitely), if AI screws this up just one time and can’t get dig itself out of the hole, guess what they’re going to have to do? They’ll have to send programmers into a giant pile of AI slop source code (which will easily be tens to hundreds of millions of lines, since AI can and does write tons of code really fast) that no one has ever even seen, try to make sense of it and fix problems with it. I can tell you exactly what is going to happen when this finally occurs, which by nature of probability is inevitable even if AI gets it right 99.9% of the time: no programmers will be able to fix it, the software will be literally unfixable, and it will have to be thrown away and restarted. There will be no other choice.
The belief that “AI will just be so good it will always handle it” is also born of ignorance of how generative AI (the transformer architecture) works. Frontier AI models currently have 1 million token context windows. The context window is a critical concept, because the way AI fakes “intelligence” is through its context window. Every single token (word or word fragment) an LLM generates in a reply is generated by feeding the entire conversation you’ve been having with it since opening the chat window (plus all the hidden static prompts the harness is decorating the conversation with) back into the neural net, which does a linear algebra transformation from this “vector” of tokens to the most likely “next token”. If it’s not in the context window, it simply doesn’t exist to the LLM. If you chop off the first half of the conversation from the context, the words the LLM will predict will be the ones that come next in a conversation that started in middle. This is when people notice LLMs acting “drunk”: they unknowingly blew past the context window (it went past 1 million tokens), so the early parts of the conversation (which probably contain critical context about what the conversation is about) get cut off, and the LLM completely “forgets” what it was talking to you about.
The context windows can’t just keep getting bigger, either. That’s a major part of the enormous cost of LLM inference, and it starts to rub up against the fuzzy “resolution” of the training data (which has also already been exhausted, the internet isn’t infinite).
1 million tokens (that’s roughly single words) is already much smaller than a lot of enterprise codebases, which often have more lines (comprised of many tokens) than this. An LLM literally can’t even pretend to “think” about these entire codebases. Humans can’t memorize millions of lines of code, but they do remember the important details. Code harnesses try to do this sort of “context compression” to simulate what humans do, but it’s just more fuzzy vector embedding guesswork. This presents an obvious reason why AI will not “just do this correctly”. It will not just flawlessly be able to make modifications to huge piles of AI slop code because those piles are much much larger than their context windows. They will start doing what programmers who can’t (or at least don’t bother to) be knowledgable in the system will do: just start monkey patching stuff, not realize it broke a bunch of far away things, and get stuck in an endless bug whack-a-mole loop.
And remember… all it takes is one time for AI to get stuck in this loop and be unable to get itself out (it will fix the bug it is pointed at, but cause 3 other critical bugs, and then when it fixes one of those, it will reintroduce the original bug), and your whole “agentically coded” software system is done, period, end of story. This isn’t unique to AI either. This is basically what’s happening, just more slowly, with most enterprise software already. The business is rushing everything, hiring a revolving door of cheap contractors who just shove tons of code into a system (rapidly growing the size to absurd levels, millions to tens of millions of lines for software that shouldn’t need more than a couple hundred thousand) until it gets stuck in a growing backlog of defects, with dedicated “bug bashes” attempting to address it just making it worse (the rushed bug fixes cause even more bugs). Most of them are in a state where only a small set of very highly skilled programmers who are experts in safe refactoring can salvage or work effectively (without making things worse) on these codebases. This is why so many of these companies are trying to rewrite their codebases from scratch, which is even less viable and almost never succeeds.
AI coding is just speedrunning this process of a software system collapsing into unmaintainability.
Now, what if eventually AI coders adopt a workflow that does make AI work like a compiler? That means they would stop checking source code into the repo, and stop writing prompts to make modifications. Instead, they would maintain a growing single prompt to “one shot” the entire software system: feed it into the chatbot, and the chatbot writes all the source code for it from nothing. It contains everything the software needs to do. This prompt gets checked into the repo, and is what gets maintained and modified in pull requests.
Well if that happens, then this prompt will become the extremely complex source of truth that needs to be managed and worked on effectively by humans. It’s definitely going to be much more than 1 million tokens, so the same context window issue is going to arise, and probably much sooner because describing software requirements in English is so verbose.
Then their workflow will be to modify this giant prompt in order to add or alter a feature. At this point, the workflow is identical to what it was before AI was involved, just substituting the source code for the prompt. And then the disaster of the software being specified in unstructured plain English will be fully manifest. Ironically, if any AI programmers attempt to do this (and probably before that, if this goes on long enough), the more clever ones will realize they keep making dumb mistakes in their prompts like typos that fail to convey two things are the same, or accidentally putting instructions in a section intended to describe the shape of data, or adding a parameter to an operation but forgetting to specify it everywhere it is invoked. And then they’ll come up with a brilliant idea: introduce keywords to annotate different parts of the prompt and write a parser that checks for mistakes like that before feeding it into the chatbot.
Congratulations! They just reinvented programming languages and compilers. Eventually one of them will realize that they’ve introduced so much formal structure into their prompt files, all for the purpose of helping them correctly write and modify their prompts, that the entire thing is parseable by a simple non-AI computer program and in fact can be directly turned into machine instructions. The parser that does static analysis is literally a language model, and once you have that, you don’t need large language models for all of natural language anymore, in fact involving one would be quite counterproductive. And at that point, they’ve squeezed the AI out of their development workflow and went full-circle back to what they were all doing in 2022.
Welcome to the Wonderful World of Metaprogramming
More experienced programmers who have done AI programming may object to all of this as irrelevant because they’ve seen AI just knock out tasks that would have taken them enormous effort to do “by hand”. This usually involves large scale refactoring operations. A popular example is translating a codebase from one language/tech stack to another. Chatbots don’t just magically do this perfectly. They claim to, and spit out a pile of code that is definitely not fully valid (and worse yet, in perfect alignment with the computerized charlatan trained to BS you, the ways in which it is not valid can be “cleverly” obscured and evade detection during a quick review, which is the only type of review possible because carefully reviewing a massive change set isn’t much easier than authoring it, especially for such “rote” work), but if you’re lucky, it takes care of a lot of the tedious parts of translating it, and you can then pick it up and fix all the issues with it.
A lot of the positive testimonials to programmers augmenting themselves with AI are some variation of this. They have some task they need to complete on a large codebase that is largely a bunch of mindless tedium (applying some kind of highly patterned change to a lot of code, which is an indication of poorly written high entropy code, but that’s the reality for a lot of enterprise systems), they already did the software engineering work of figuring out what design/architecture they want to get to, and they just want to hand the menial execution off to something else.
So here’s the thing about that. Programmers absolutely should not be wasting their valuable time and talent mindlessly typing repetitive patterned changes. But do they need AI to automate this tedium? Well… no! What they need is the skill of getting a computer to knock out a mindless, repetitive task quickly and flawlessly instead of doing it manually… which is always what computers have been for. You know what skill is called?
Computer Programming!
In other words… when a programmer realizes he needs to execute a loop of repetitive modifications to code (or to generate a lot of repetitive boilerplate code, which like I said, he usually shouldn’t be at all, unless the goal is to get the compiler to enforce a pattern of rules that the language doesn’t support expressing directly), instead of manually typing it all out, he should write a computer program to do that for him. Writing code to write or modify code is a concept nearly as old as programming itself. It’s what assemblers and compilers fundamentally are to begin with. And approaching programming tasks this way, with a mindset of reflexively applying the programming skill back on programming itself, is extremely under-appreciated and undernourished in the industry. I didn’t start doing metaprogramming until around 12 years or so into my professional career. I had thought about it for years but kept dismissing it as “too hard” or “not worth the trouble”, until I actually tried it out, and couldn’t help but laugh once I realized how much time I had wasted up until then.
The specific example where I did this was to write a program that takes an OpenAPI spec of a web service used by all the clients of a software system (mobile, smart TV, etc.) and generates the strongly typed client code to call it. This work of writing the “glue” code for a mobile app to consume the web service by hand constituted probably 30% or more of my time in the years I did mobile app development, and this wasn’t peculiar to me. It’s just how it’s done. But I had some downtime on a project and decided it would be an interesting experiment to try to hand this off to a utility program. The OpenAPI spec was available in YAML format, so I need to write a program that parses YAML, then loops through the structures describing the endpoints and shapes of request bodies and responses, and generate source code (Swift in this example) from it. How long did it take me to write this program and get it to fully generate working client code?
One day. One freaking day!
In one day, I accomplished what the rest of the client developers had to repeatedly spend a significant part of their effort on doing themselves (since the project was early on and the web service was still very volatile and incomplete) for the entire project. Every time I needed to implement a feature that needed a part of the web service newer than the last time I hit the “generate” button, I just hit it again, and in a couple seconds I had all the code I needed to call the service.
Roughly in parallel with this, I had also encountered a few cases of needing to translate an app from one language/stack to another. The first time I ever did that, I did it pretty manually. But this, more than anything else, forced me to start learning more about regular expressions (regex), which almost every IDE supports as a way to issue patterned find-replace operations. The next time I had to do this, I had reduced most of the tedious part to firing off a sequence of standard regex find-replace commands (to, for example, convert variable declarations in the form of Type name to name: Type, or replace function declarations of the form ReturnType functionName(...) with function functionName(...) -> ReturnType, etc.), leaving only the “interesting” parts of substantial differences in platform libraries that shouldn’t be handed off to a mindless algorithm for me to attend to.
Metaprogramming is a wonderful thing, especially when working on big, messy legacy codebases. The extent of what you can accomplish with it depends only on how seriously you want to invest in tooling for it (some tech stacks like .NET, which comes with a lossless parse tree tool, a part of the compiler Roslyn, to analyze and modify code in structured format, already have some). If you can parse source code into syntax trees and render it back to source code (what the aformentioned .NET tool does), it’s surprisingly easy to execute very sophisticated refactoring or patterned modification operations. Taking data in various formats (web service specs, database schemas, data structures in e.g. JSON, etc.) and rendering patterned code from it is quite simple (it’s just emitting lines of interpolated strings) and extremely powerful. Again, I’m surprised programmers (including me) don’t do this more. It’s probably downstream from software companies aggressively dissuading any amount of investment into tooling or maintenance and instead shouting at programmers to build the next feature right now.
Whatever you can accomplish with AI in this regard, you can accomplish it with traditional code and have it be much more reliable and cheaper. You will be able to precisely control what it does, it won’t fool you with convincing but subtly wrong outputs or randomly go off script and start doing things you don’t want, and you don’t have to write a check to Dario every time you execute it. The reason why programmers get impressed with AI in this area is because their own skills in that area are quite underdeveloped. They would be much better served developing those skills further than just telling a chatbot to do it and hoping it works out.
This is also true of scripting. There are various tedious little tasks programmers have to do routinely that aren’t writing software. Some programmers will tell you handing that stuff off to AI is a good way to utilize it. Sure, that makes a lot more sense than “agentic engineering” where you bend over backwards to create code without ever thinking about, writing or looking at code, but once again, scripting is just another programming skill.
Software Factory!?
The Final Boss of all the confused nonsense emerging out of the big rush to shove a chatbot into programming in a quest to “automate as much as possible” is the idea of a “dark factory” for software. It’s a fully autonomous system you just feed requirements into and software comes out the other end like a conveyer belt. I can attest from direct experience that software companies are spending significant time and money trying to build something like this. Like I said at the beginning, the intention the non-technical parts of the companies have in this is, once the factory is operational, to fire either the entire engineering department, or all but a skeleton crew to occasionally tighten screws on the factory as needed. I don’t know what the programmers working on this think is supposed to happen.
But the point is putting the two words “software” and “factory” next to each other succinctly captures how completely confused people have gotten about these concepts. Those two words are almost polar opposites, standing in complete contradiction to each other.
A factory is something that takes a blueprint and just churns out identical copies of the real thing the blueprint describes. For example, a factory might take a blueprint for a car engine, and produce engines according to that blueprint repeatedly. A factory operation is literally a loop, and in fact is built very similar to how computers (specifically CPUs) work: a pipeline that stages the operations, where each stage repeatedly loops on its specific job, taking inputs from the previous stage (or the blueprint if it’s the first stage) and producing the inputs for the next stage (or the output if it’s the final stage), which is exactly how CPUs loop over instructions of a program and execute them in stages.
What is “software”? It is literally a blueprint! It is a blueprint for what a computer should do. What would it mean for a “factory” to create software, or blueprints? It’s quite simple. We all have “software factories” at our finger tips. Here, do this: go find an .exe or .app or whatever file on your computer (whatever an application or program file type for your operating system), hit Ctrl+C and then Ctrl+V (or Cmd+C then Cmd+V). What just happened? There’s a copy of the file for the program. So now there are two copies of the app! You just manufactured a computer program from a blueprint!
Yes, a “software factory”, if it means anything at all, means copying and pasting.
But that’s obviously not what people have in mind. They think a “software factory” takes a… blueprint… for what, exactly? And then it… produces something repeatedly in a loop. That thing it produces isn’t a particular (already existing) computer program, it’s a new computer program, a .exe or .app that didn’t exist at all before the factory ran. And on every loop it produces new computer programs, each one being new and unique, just like each engine the car engine factory produces is a new and unique engine that can be put into a car.
But… every engine is exactly the same. That’s the whole point! That’s why the factory can run in a loop: because each iteration of its operation produces something identical. Each engine that it produces is “new” and “unique” in a physical sense, which is why it’s useful. It is, literally, implementing Ctrl+C -> Ctrl+V but for a physical thing like an engine. If I have two cars, I need two engines. If I have one, I can’t simultaneously put it in both of my cars. I would need to copy it, which means to take a blueprint or specification for the engine I already have and use it to drive a manufacturing process that produces another actual engine. With physical things, “factories” looping over this operation of producing identical real physical copies of an abstract concept is useful.
But a “software factory” is more like a factory that produces… engine blueprints? So it doesn’t churn out actual engines in a loop, it churns out pieces of paper (or USB sticks, or just saves a new file to a network drive or something?) containing the design for an engine. But if it’s a loop, it’s repeating, which means it’s producing the exact same design every time. So… it’s a file copy operation! Yeah, we already have that, and it works for all files ever (files are just bytes and computers can already copy bytes). The idea that each iteration produces a different design, a novel design for a new engine, like… on iteration 1 it produces the design for the next year’s sedan inline 4 cylinder engine, on iteration 2 it produces the design for the next year’s sedan high trim V6 engine, on iteration 3 it produces the design for the light truck V6 engine, on iteration 4 it produces the design for the medium truck diesel engine… this completely contradicts what a factory is, because it’s not a loop. It’s producing something completely different each time. Nothing is being “repeated” here.
And what would be the blueprint you feed into this “factory”? A blueprint for… making all the engines conceivable? Or… making an unspecified engine? But then you’d had to specify which engine you want, and then you’re feeding a series of specifications into the “factory”, and what would those specifications be except the blueprints for the engines? In other words, what do people imagine being fed into this “factory” on each iteration other than precisely what they want to come out of the “factory” on each iteration? And in that case, what is this “factory” doing (passing through a design document from one place to another)? You have to already design the blueprint to feed it in, so the factory certainly isn’t “building” the blueprint for you.
What do people imagine feeding this “software factory”? The answer is requirements for a specific software system, right? Well as I’ve been harping on this whole time, that’s the software! If you’ve done all the work to fully specify the system, then you already did all the work you’re expecting this “factory” to do. And again… what is the “blueprint” that drives this factory? A blueprint for… all software, ever? Well that’s called a computer (a general purpose mechanical device that can execute a set of instructions in a sequence, which means it can execute any computer program fed into it). But we’re not talking about hardware factories, so… there’s just nothing here. “Software factory” is an empty concept. “Factory” implies a repetitive loop turning a blueprint into identical real copies of a thing, and “software” is unique blueprints for instructing a computer to do something (which it can do as many times as you want, you can double click an .exe repeatedly).
The fact people ever started talking about “software factories” makes it blatant this is all just tech babble nonsense by software company executives and product people having fever dreams about making “all the money” while doing essentially nothing. It’s gone completely off the rails and we shouldn’t be indulging this (which we’re doing because a few AI labs full of weird cultists who think GPUs have feelings if they process natural language have a lot of money right now).
Conclusion
Programmers delegating “coding” to something else, whether it’s a chatbot, another type of computer program, another person, etc. is confused because “coding” means precisely instructing a computer to do something. You’re still doing that, but with unnecessary, unreliable and expensive extra steps crammed in. It’s really just throwing away the more than half-century of foundation we’ve built up for how to do that most effectively. There’s a reason we code in programming languages, not English.
In the final part of this series, I will discuss the issue of coders who hate coding and make it their mission in life to work as a professional “programmer” but do as little programming as they possibly can, which is a large part of why this has gone as far as it has.