What Vibe Coders Need to Know About Thinking Like a Programmer
I completed a type of introductory computer science that was supposed to teach me how programmers think. I learned about algorithms, abstraction, control flow, functions, data structures and systematic approaches to problem decomposition. However, I did not achieve fluency with any one programming language sufficient to develop and deliver substantial software on my own.
As a result, the course seemed to me slightly incomplete. I had mastered much of the theory, but not the professional experience necessary to validate it. I understood why a given problem should be decomposed into functions, but was unable to easily translate the complete solution into Python or JavaScript. My knowledge of the function of variables, loops and conditional statements was sufficient to require continued reliance on documentation, examples and expert programmers to achieve any degree of complexity.
Finally, we arrived at "vibe coding."
Suddenly, the task with which I had previously experienced only limited success (conversion of an idea into working code for a specific programming language) could be largely delegated to an artificial intelligence. This did not invalidate my previous experience, but in fact encouraged me to use it with significantly greater awareness.
Use of an AI-based system for programming always resulted in much less concern with syntax. Instead, I concentrated on the intended functions of the system, on the components required for its operation, on the sequence of events, on requirements for persistence of information, on expected changes after an interaction and on requirements for separation of different system components. In effect, I continually translated an imprecise overall goal into more concrete requirements for behavior.
Finally, I used extensively the type of high-level, solution-oriented thinking developed in web design. We always worked in a reverse direction from that used by programmers. Our concern was with information architecture, patterns of user interaction, methods of navigation, principles of visual organization, strategies for development of reusable components, limitations imposed by characteristics of different devices, definitions of states of interaction and of conditions in which information was missing or user behavior was unexpected. Ultimately, we achieved experience in thinking about systems rather than about isolated components. My ability to recognize a malformed loop or to explain a line of JavaScript is doing far less intellectual work for me than is the background knowledge underlying the art of vibe coding.
Introductory computer science was never only about learning syntax
Much public discussion of vibe coding has focused on a different problem: People who lack knowledge of programming may fail to detect errors, vulnerabilities or extremely poor implementations of software. This is a legitimate concern, but one that is now familiar. Our repeated messages risk limiting our understanding of what non-programmers actually lack.
Determining whether generated code is correct represents only a small part of our programming judgment. We must first determine what the code is intended to mean before we can judge it as correct or incorrect.
The more interesting question, therefore, is not simply whether we can observe the output of the machine. Rather, it is whether we have developed the higher-level capacity for programmatic thought characteristic of the first year of computer science. This is the capacity to translate an intention into a coherent model of computation.
Our instruction in programming languages is certainly an important component of this experience, but it is always secondary to the overall learning experience.
The experience of our introductory course at Harvard is an excellent example. There, students are taught to solve problems "with and without code," with an emphasis on design, correctness and style. Major topics include experience with computational thinking, abstraction, algorithms and data structures. The course clearly separates learning about the fundamentals of programming from learning about a particular programming language.
Our experience with the introductory programming course at MIT is similar. Although instruction is provided in the Python language, the overall objective is to give students an understanding of the role that computation can play in solving problems, and to provide students of different disciplines with sufficient confidence to develop simple programs that achieve useful objectives.
The language is only a vehicle for achieving these objectives. It is through the experience of representing and solving problems that students develop the capacity to describe and test the behavior of proposed solutions.
Finally, the experience of developing these skills is of greatest importance in an era in which the machine can often provide the language. An API can generate a loop without the user knowing where the brackets should go. But this does not necessarily indicate that it has taken over the central intellectual activity of programming. Rather, it has assumed responsibility for transcribing a computational idea into a language for programming.
The human remains responsible for developing the idea.
As a result, first-year instruction in computer science will become more important, not less important, in an age of automatically generated code. Its most valuable contribution is not the ability to type code from memory, but rather experience with making a problem computationally intelligible. The first requirement is the ability to decompose a complex, integrated problem into smaller problems that are amenable to individual understanding and solution.
Decomposition: knowing what the problem is made of
Computational thinking refers to the process of decomposing complex, interrelated problems into manageable components and developing systematic approaches to their solution. The process emphasizes decomposition, pattern recognition, abstraction, generalization, componentization and automation.
For a "vibe coder," initial experience with decomposition may appear to be little more than a division of a large request into several smaller requests. However, true decomposition is considerably more extensive.
Specification: deciding what success means
Closely related to decomposition is the ability to describe behavior precisely.
An inexperienced builder generally begins with an image of successful results: a working interface, an animated system or a chatbot that appears to respond to questions. The programmer then replaces this visual impression with explicit conditions.
What inputs are required? What outputs are expected? What conditions must always hold? Which inputs are valid? What behavior occurs at boundaries? When is a task complete?
An algorithm represents a sequence of sufficiently precise instructions for converting inputs to outputs. Accuracy is essential because computers cannot silently apply common sense to fill gaps. Generative AI effectively masks this limitation by providing excellent estimates of intended behavior. However, the resulting software ultimately must operate according to well-specified rules.
This is one of the aspects of vibe coding that appear to be unusually successful. A loosely formulated request results in an impressive demonstration based largely on reasonable assumptions by the system on behalf of the user. These assumptions become apparent only when the project is expanded or encounters conditions not anticipated in the original request.
Conventional programmers would refer to these requirements as acceptance criteria, contracts or invariants. Designers would recognize them as defining the complete set of states of interaction and results of use. The terminology is less important than the discipline of expressing intended modes of operation explicitly.
Prior to requesting AI to implement a feature, a practitioner of vibe coding should be able to describe:
the nature of inputs; the nature of outputs and changes in state; conditions that must exist prior to implementation of the feature; conditions that must exist at completion of the feature; events that must never occur; and conditions of behavior that should result when the feature cannot be completed.
This represents not primarily a skill of code review, but rather a capacity for describing modes of operation.
Abstraction: thinking at the right altitude
Abstraction is generally considered to involve disregard for irrelevant details in order to focus on important aspects of the problem. This appears to be a simple task, but in reality represents one of the most complex skills of programming.
All software projects can be characterized at several levels of detail. For example, a button is a colored rectangle. It is also an interactive component that represents an event trigger, a transition between states, a request for a service or initiation of a business process.
The task is to determine which level of description is appropriate at each moment.
A high-level vibe coder who is not sufficiently detailed may state, "Make the booking process work smoothly." The AI must then develop most of the implementation mechanisms. In contrast, a low-level vibe coder who specifies too much detail may spend hours describing file names, conventions for the target application and details of individual functions, without ever completing the design.
Experience as a programmer involves deliberate transitions between levels of description.
At one level, I decide that a learner should be able to submit an application. At the next level, I identify the information that must be collected. At lower levels, I specify validation requirements, assign responsibility for collection of individual data elements and determine which events will invoke which functions of the target application. Finally, I may require the AI to make choices among alternative implementation approaches.
This experience with levels of description is one reason that individuals trained in web design, service design or business analysis often adapt rapidly to AI-assisted development. They are accustomed to converting broad objectives of client needs into requirements for screens, processes, components and content.
Consequently, I must not consider a requirement for "a web-based enrollment system" to be a sufficiently complete description of the desired functionality.
The extended application of experience with levels of description leads to additional requirements for description of the behaviors and transformations that make it possible to display information on screens.
My resulting description of the design task therefore is not simply a more detailed statement of requirements. Instead, it reflects a decomposed representation of the design. The system might ask the AI first to identify entities, define relationships, describe the user journey, specify system states and only then to implement the solution.
Decomposition of a problem cannot be completely achieved by AI because decomposition reflects priorities. There are generally multiple reasonable approaches to decomposing a system. The optimal method reflects what I am actually attempting to optimize with respect to speed, usability, maintainability, privacy, flexibility, educational value or other factors.
The classic computer science text Structure and Interpretation of Computer Programs is organized around the development of abstractions with procedures, with data and ultimately with modular objects and state. This reflects a fundamental insight about complex systems: They become manageable when we can define meaningful levels of abstraction and reason about them without continually re-opening all details of internal structure.
This experience is equally familiar to web designers. Use of a design system enables me to work with a card, a component for navigation or an alert without again considering all details of borders, spacing values and styles of text. The same principles apply to abstraction of behavior and data.
The major skill required of a vibe coder is not necessarily the personal construction of every abstraction. Rather, it is the recognition of the need for an abstraction, the determination of the responsibility that it should encapsulate, and the detection of situations in which two distinct concerns have been improperly combined. Only then can the system achieve an adequate representation of its simplified view of the world, including decisions about which entities exist, what properties describe them, how they are related and which distinctions are important.
Data modelling: deciding what the system believes exists
Participants of a course, multiple enrollments of a participant, distinction between participant and enrollment and separation of these data into different spreadsheet rows illustrate the importance of separating different concerns. Similar considerations apply to separation of author and content for blog entries, of date, status and owner for records of bookings, and of data and data manipulations for all other forms of information.
Finally, the ways in which information about cancellation is stored ultimately determine what explanations can later be given for events that occurred in the past.
This is not a problem of language specificity. The same questions of representation arise for SQL, for JSON, for spreadsheet applications, for content management systems, for form builders and for no-code development environments.
What are the main entities?
Which facts belong to each entity?
Which relationships are one-to-one, one-to-many or many-to-many?
Which values are optional?
Which values must be unique?
Which facts change over time?
Which history must be preserved?
Poor representation of data results in problems that cannot be completely eliminated by sophisticated programming techniques. Information is duplicated, records become inconsistent, relationships are ambiguous and minor changes require extensive reprogramming of all components of the system.
Use of artificial intelligence to generate a representation of a database structure or of object relationships is of no greater value than conventional methods of representation. In neither case is it possible to obtain an accurate description of the meaning of concepts within the domain of the user. Rather, both methods of representation produce results that are technically conventional, but do not reflect the actual experience of the user with the system. Data modeling is where high-level design becomes operational. It defines what the software is capable of knowing.
State: understanding that interfaces have memory
State refers to information that a system must retain in order to behave appropriately at a given instant.
A menu is open or closed. A user is authenticated or anonymous. A form is untouched, invalid, in process, successful or in error. A document is a draft, under review, published or archived. A request is pending, completed or aborted.
These are not merely aesthetic variations. Rather, they represent states of the system for which actions produce transitions.
Web designers routinely deal with states, but often treat them as secondary representations of the interface. Programming requires that these states be represented explicitly, and that rules be defined for transitions into and out of each state.
Modern browser interfaces are event-driven. The browser reports that some event has occurred (e.g., a mouse click, key press, form submission, completed page load or error), and code responds to that event. With the addition of asynchronous operations such as network requests, the temporal sequence of events becomes part of the design. The resulting model of execution for JavaScript, for example, employs queues and an event loop to schedule activity after expiration of timers, completion of input/output operations and other events.
The resulting mental model eliminates the need for a coder to recall and reproduce the details of the event-loop specification. However, it does require understanding that interactive software is much more than a document with a few buttons attached. Rather, it represents a continuously changing system that responds to events over time.
This fundamentally alters the way we prompt. Instead of requesting simply that a submit button be added, we might specify:
The form is initially in an editable state.
Successful submission of valid data results in a pending state of operation. Repeated attempts at submission during this interval are prevented. Receipt of a successful response results in a state of confirmation, whereas rejection of a response returns the system to an editable state of error with retention of all previously entered data.
This represents a style of programmatic thinking in which behavior is described independently of the ultimate implementation.
The importance of a mental model of state is especially great for the practice of vibe coding, because initially generated interfaces generally appear to be correct. Deficiencies in behavior become apparent only when interactions occur in atypical sequences. The resulting patterns of control flow describe the paths of execution through a program.
Control flow: tracing what happens and under which conditions
Even with complete responsibility for generating all syntactic details, it remains necessary to understand the general forms of control. These include sequential, conditional and iterative modes of operation.
Some event is followed by another event. Execution of one path of operation is conditional on the occurrence of some event that excludes execution of an alternative path. Each item in a sequence causes repetition of some operation. The result of a named transformation determines the nature of subsequent events.
These represent elementary concepts that provide a basis for modes of reasoning. It is generally possible to identify the underlying conceptual problem without reading all of the code. For example, two conditions that should be mutually exclusive may actually be simultaneously true, or an action may occur before some required operation is performed. Existing data make it possible to apply loop logic in a way that affects all records where it should apply only to a subset selected by a filter.
Control-flow literacy enables users to describe the operation of the system in plain language.
It also facilitates the design of prompts. A well-designed prompt often resembles pseudocode even in the absence of formal syntax:
For each course, compute the number of active enrollments.
If the number is less than capacity, make the course available.
If capacity is achieved, but a wait-list option is available, make the course available only on a wait-list. Otherwise, make it unavailable.
The system can translate these specifications into code. The responsibility for establishing the rules lies with the human.
Invariants: identifying what must never become untrue
An invariant is a condition that must remain true throughout a process or at a particular point in it.
Each booking must represent an actual user. Balance changes must never occur twice for the same transaction. Student records must not simultaneously reflect withdrawal and completion of a course. All published articles must include an assigned title and date of publication. All records must be accessible to an administrator, but only records for an individual user must be accessible to ordinary users.
These requirements reflect a level of system integrity that is greater than that required for interface design.
Developers of intelligent systems must learn to search for invariants because the capacity of the AI to implement individual operations without maintaining consistency for the entire application is very great. The resulting function for "deletion" may be correct, but what should occur with related records? Similarly, the resulting status change may be correct, but which prior status conditions are allowed to result in the new status?
This represents a situation in which expertise with respect to real-world processes is more important than expertise with respect to programming techniques. The individual who best understands the real-world process is best able to specify conditions that must never be allowed to develop into inconsistencies.
Implementation of these requirements reflects the necessity of describing these conditions explicitly and enforcing them centrally rather than distributing them among components of the interface. Most effective systems are assembled from component parts, including components, modules, databases, external services, browser interfaces and commercial systems.
Interfaces and contracts: understanding the joins
Integration of these component parts represents an area of continuing research. Each interface defines a contract for what one side provides, what the other expects and what occurs when the agreement is violated.
A vibe coder need not memorize every format for an API. Instead, it is important to understand that integration is not magic. Information is transmitted across a boundary in a particular format. Operations require specific inputs and produce outputs that may be successful, failed or delayed. Authentication identifies a user and determines what actions that user may perform.
High-level thinking about a system focuses on these boundaries.
Where does information originate?
Which component is responsible for it?
Which service may modify it?
In what format is information transmitted across a boundary?
What assumptions are made by each side of the transaction?
What occurs when one component changes?
This represents another aspect of web design. Designers routinely deal with boundaries between content and presentation, between desktop and mobile systems, between browser and server, between component and page and between user and interface. Programming provides additional rigor for these boundaries, but conveys a capacity for separating concerns that is applicable to other areas of computing.
Algorithmic thinking: designing a process rather than requesting an outcome
An algorithm describes a precisely defined method for converting an initial condition to a result.
Most skill with advanced methods of algorithm design is not immediately required of a vibe coder. It is unnecessary to rediscover methods of sorting or to solve problems involving traversal of a graph in order to construct a useful web site.
Nevertheless, experience with the principles of designing processes is essential.
Achieving the most relevant result is a goal of the process. Filtering to eliminate records that are not eligible, computing a score based on three factors, ranking the remaining records according to that score and returning the five highest-ranked records represent an algorithmic description of the process.
The second description reveals assumptions that can be challenged, modified and evaluated.
Algorithmic thinking also involves identification of patterns that can be generalized. If I repeatedly solve similar types of problems (formatting records, applying rules, transforming data or computing status), I should consider whether the process can become a reusable operation rather than another isolated instruction.
This represents a level of AI coding that exceeds iterative prompting. The user develops a representation of recurrent processes within the system. Experience with introductory courses in computer science often illustrates the difference in effort required to solve problems of increasing size.
Complexity: anticipating what happens when the project grows
The practicing programmer need not compute formal classes of complexity for every function, but must develop awareness of the effects of scale. A process adequate for 10 records will be inadequate for 10,000. Repeated retrieval of identical information from a remote server is acceptable for demonstration purposes but slow and costly in an operating system. Retrieval of full-resolution images will appear acceptable on a developer's desktop, but will fail for a mobile connection.
Useful questions for beginning programmers are practical:
Does the amount of work increase with the number of users, records or components? Is the system performing work that could be accomplished once? Is unnecessary data being retrieved or processed? What occurs when the volume of data is 10, 100 or 1,000 times greater?
AI will optimize the code in response to these questions, but will require that the human observer recognize the importance of growth to the problem. Evaluation of a prototype is based on its ability to operate in the present. Evaluation of a system also reflects the potential for future modifications.
Modularity: designing software that can survive change
Division into modules of well-defined responsibilities and readily understood relationships permits modification of one component without requiring complete reimplementation of the entire system.
This is more than a matter of clean code. It reflects an awareness of major boundaries of design. The module that displays a course should not also contain all rules for computing eligibility. The function that validates a record should not control the format of an error message. The data-access layer should never silently depend on the current page layout.
As with component-based design, a well-designed web component has a purpose, an interface, variations and constraints that allow reuse without replication of its internal structure.
Vibe coding increases the importance of modular design, because AI tools can add features with extraordinary speed. Each addition appears to be locally appropriate, but ultimately leads to a system in which responsibilities overlap and every file depends on every other file.
Programmers must acquire sufficient awareness of the architecture to ask where a new behavior should be implemented, whether an existing responsibility should be extended and what other components of the system are authorized to know about these changes.
Testing as clarification, not merely fault detection
Use of testing is primarily intended to detect errors. However, its principal value for vibe coders is to force explicit representation of intended behavior.
The approach to designing programs teaches students to analyze the problem, specify the involved data, formulate a purpose, work out examples, develop a structural plan and finally derive tests from their examples. This experience develops skills of analytical reading and writing, because the programmer must convey the essence of a problem in a precise manner, plan a solution and evaluate the results against expectations.
A test begins with a proposition about meaning:
This is in contrast to the effort of searching through generated code to detect errors. As a consequence, the following results should be achieved:
The result should be empty, not an error, when the input is an empty collection. The operation should be refused when the input represents a user without appropriate privileges. The system should not generate a duplicate response to a repeated request.
The formal code for all tests can be generated by the AI. The programmer remains responsible for determining which proposition is correct.
Finally, testing represents a component of the specification. It provides a means of making the programmer's model of the system amenable to inspection.
The advantage web designers may already possess
My experience with these concepts supports my view that the principal distinction is not between programmers and non-programmers, but between those who have substantial knowledge of programming languages and those who do not. Some designers, analysts, researchers and educators have spent years analyzing ill-structured problems, modeling user behavior, defining workflows and conveying the meaning of complex systems.
Web designers in particular operate in a hybrid mode of representation. They represent content as structured information, not as decoration. They develop reusable visual abstractions, anticipate responsive transformations, design interactive sequences and distinguish global patterns from local exceptions. Ultimately, they negotiate among requirements for user satisfaction, technical feasibility and organizational objectives.
Even my discussion of how to design programs uses web designers as an example of professionals for whom the process of designing programs represents a form of programming. In so doing, I reinforce my general argument that systematic methods of program design have value far beyond the occupational role of programmer.
Web design does not provide a complete model of computation. It does not teach its practitioners to reason explicitly about mutable states, asynchronous events, integrity of data, rates of algorithmic growth or relationships between services.
However, it provides a strong basis for viewing products as well-organized responses to human needs. The next challenge is to extend this perspective to levels below the visible interface.
What AI can supply the Vibe Coder—and what it cannot decide
AI coding tools are exceptionally effective at providing local knowledge of implementation. They retain syntax, generate boilerplate, invoke libraries, explain unfamiliar code, translate between languages, generate test scaffolding and suggest common architectural patterns. As a result, they substantially reduce the amount of expertise in programming languages required to begin developing software.
However, they are less effective for problems involving meaning. They cannot independently determine which aspects of the real world are important to our users, nor can they identify which rules of organization are inviolable without explicit instruction. Consequently, they cannot decide whether preservation of historical data is more important than simplification of the schema, nor can they resolve an ambiguously stated requirement by producing a plausible implementation.
Instead, they make assumptions and rely on their ability to generate apparently reasonable results to mask the absence of a well-developed model of human behavior.
Ultimately, the skill of the vibe coder is not to become a slower, less fluent replica of a conventional programmer. Rather, it is to develop greater capability for designing computational representations of intentions. This involves:
Converting requirements into explicit behaviors, Partitioning systems into meaningful components, Representing and relating appropriate data, Describing and representing states and transitions, Representing and accounting for decisions and repetitive operations, Identifying conditions that must always hold, Defining boundaries of separation between components, Predicting and accommodating future growth and changes, and Finally, achieving and demonstrating the expected results prior to implementation.
These are not marginal skills of little relevance to modern instruction in programming. Rather, they are the skills required to allow AI-based implementation to produce a system rather than merely an assembly of apparently successful fragments. Consequently, an appropriate mode of instruction for vibe coders would not require many months of intensive training in the syntax of programming languages before the student was permitted to develop any system.
Instead, it would use AI-assisted development as the vehicle for making explicit the process of thinking about programming. The student would initially describe a simple interactive system in ordinary language, identify all components, inputs, outputs, states, events and conditions of permanence, represent the flow of information between components and express important rules as pseudocode and expected behaviors as examples.
Only then would the student request implementation of the system by AI. As the system evolved, they would describe not every line of syntax but the overall shape of the solution. Where is state stored? Which component is responsible for which behaviors? What is duplicated and what happens when an operation fails or arrives late? What assumptions underlie the data model and what costs increase with the growth of the project?
This would not eliminate the need to learn some programming. Interpretation of results, minor modifications and recognition of familiar programming constructs remained valuable, but would support rather than define the overall goal.
A different curriculum for the vibe coder
The goal was to produce individuals who could think sufficiently about computations to work effectively with a machine that implemented their designs. The future distinction would not be between individuals who could and could not write programs from memory.
Rather, it would be between individuals who could translate an intention into a consistent representation of a computational system and those who could only request that the machine continue to try until the output appeared acceptable.
A programmer with an understanding of computational processes directed the work. He or she determined what components should be included, how they should be related, which behaviors should be allowed, what meaning should be assigned to the data and what constraints should remain in effect as the software was modified.
In the absence of such an understanding, the user remained dependent on the machine for both implementation of the design and definition of the overall character of the work.
This represented the major value of the first year of experience in computer science. It did not merely teach the ability to detect errors in programs. Rather, it taught appreciation for the necessity of analyzing, simplifying, representing and converting an ill-structured aspect of the real world into a sequence of decisions that could be executed by a machine.
The real divide in vibe coding
Finally, I will never become a programmer who can develop an entire application with complete fluency from a blank file. But I no longer believe that this indicates failure of my introductory experience with computer science. Instead, it demonstrated repeatedly that I was capable of stopping my requests for assistance and beginning to work independently. It emerges when I break down a large goal into component parts, separate data from presentation, track the states of an interaction, challenge an assumption or require specification of the results prior to writing code.
Vibe coding has not invalidated that first year of training.
But at last, it has demonstrated what I was learning.