UseAIHereUseAIHere

From Vague Requirements to Maintainable Systems: The Ins and Outs of Software Engineering — Lecture 5 of Nanjing University's Generative Software Engineering

September 22, 2026 · UseAIHere

阅读中文版 →

From Vague Requirements to Maintainable Systems: The Ins and Outs of Software Engineering — Lecture 5 of Nanjing University's Generative Software Engineering

Course page: The Ins and Outs of Software Engineering · Video

Overview of the main points of the fifth lecture

Generative AI can already start from an empty project and one vague requirement, quickly fill in the interface, data structures, exception handling, and tool calls, and produce a demonstrable system. Once the cost of implementation falls, software engineering ought to lose its reason to exist. What actually happens is the opposite: implementation gets faster, more decisions stay hidden, and wrong assumptions spread into code, tests, and deployment more quickly.

Software engineering has always dealt with one problem: how to turn human intention into a system that works, can be verified, and can be maintained. AI changed one stretch of that translation process without deciding requirements, constraints, failure boundaries, and acceptance evidence on anyone's behalf. Understanding the ins and outs of software engineering is what shows which work can be handed to an agent and which decisions have to be preserved explicitly.

1. Where the problem moves once implementation costs fall

The capability boundary of models keeps moving quickly. Real-time decision models can output actions from structured state with very little latency, without generating a word-by-word explanation for a person the way a traditional large language model does; stronger models have also started to handle spatial relations, tool operation, and cross-modal tasks. For software production, this means many details that used to be filled in by hand can now be generated by a model first and corrected further through run results.

Being able to build something is not the same as it being worth building. Models chase immediate visible feedback and finish the current task the fastest way, and they may not judge which problem is most worth solving or which trade-offs will cost something in long-term maintenance. Taste, requirement clarification, and system boundaries do not look as easy to verify as sorting or compilation; that only means the feedback comes later, costs more, and carries more noise, and it does not put them beyond engineering. They can be engineered once the capabilities sit in an observable environment, where experiments, task results, and independent checks form the feedback.

Training itself can extend along the same path: build generators, simulators, and verifiers for new domains, and let models get comparable feedback from a richer data distribution. Whether recursive self-improvement forms an effective loop depends on whether each round brings a net gain that has been independently verified; a model giving itself a higher score does not mean its capability really improved.

The corresponding answer in software development is to build a minimum viable product first. The value of an MVP is to turn the most important intention into something clickable, runnable, and open to criticism as quickly as possible, using as little implementation as possible rather than shrinking the final system into a toy. Once a prototype exists, the assumptions that were hidden in people's heads come out: whether the interface matches the way of working, whether the data should be stored this way, what users need to see when something fails, and where the system boundary should sit.

A self-service prototype for physical devices shows this change. Scanners, label printers, and other lab equipment each come with hard-to-use software and drivers, and an agent can abstract them, through repeated attempts, into cleaner interfaces such as scan and print, which an upper-layer interface then combines into a unified flow. What transfers here is hiding concrete implementations behind stable interfaces and decoupling intention from implementation, rather than the driver trick for one device. AI lowered the cost of a first version without replacing interface design and verifiable results.

2. The software crisis comes from the gap between three spaces

Software engineering starts from three spaces: Intent is the problem a person really wants to solve, Specification is an explicit description of acceptable behavior, and Implementation is the code, data, configuration, and runtime environment. Writing the code out is only part of the difficulty; the three spaces drift apart from one another easily.

In early software development, programmers and computers were both expensive, and programming tools were immature. Clients often knew what they wanted to improve without knowing how a computer could implement it; programmers might be strong on algorithms and machines without understanding the client's business, institutions, and allocation of responsibility. The job of a software company was to translate the intentions on both sides into a deliverable system.

Software requirements are also highly customized. For the same course-registration system, different schools may use first-come-first-served, a lottery, or slots reserved by major; whether a student who lacks a prerequisite can get special approval, and what happens to a slot after a drop, are business rules too. The natural-language phrase "build a course-registration feature" does not finish the requirement. Education, payment, approval, and permission systems all contain many exceptions determined by social institutions and human preferences, and these cannot be derived directly from natural laws the way the material strength of a bridge can.

As requirements multiply, schedule, cost, and quality can all run out of control together, and that is the background of the software crisis. Faster hardware did not remove the problem by itself; it let people attempt larger, more complex, harder-to-predict systems. Software is the projection of the physical world and social needs into the information world; the projection rules can change, and requirements keep changing in use, so delivering software means handling technical constraints, human intention, and later maintenance at the same time.

Apollo software gives a stricter meaning to "it works". The lunar landing system faced limited resources, tight deadlines, and unreliable hardware, and a correct path was only the minimum requirement; when something abnormal happened, the system still had to keep the critical mission alive. When a program alarm appeared during descent, priority scheduling and recovery mechanisms kept the critical control running, provided those mechanisms had been designed and tested beforehand. Defensive design takes "things that should not happen can still happen" into the system boundary, instead of assuming every error is impossible.

Software engineering therefore studies how to turn human intention into software that runs, ships, and can be maintained, under constraints of cost, time, team, and technology. Requirement communication, design, code, tests, team collaboration, and small coding conventions all belong to this work, because together they decide whether the system keeps working.

3. What the waterfall chart actually warns about

Software engineering textbooks often show a waterfall chart running from requirements analysis, design, coding, and testing through to delivery. It is usually read as a strictly one-way process, and the point of the classic paper was that pushing large software through in that direction carries very high risk.

Testing is the first event on the whole chain that physically happens. Before testing, the requirements document, the design document, and the code are only descriptions or implementations of future behavior; constraints such as time, storage, input and output, concurrency, and failure recovery are really experienced only once the system runs. The analysis stage can propose an algorithm, and the testing stage may find that it cannot finish within the required cycle at all. Deleting a few instructions does not solve that, and the design, the division of tasks, and even the commitments made in the requirements may all need rework.

The later a defect appears, the higher the rework cost. If an online judge problem turns out to have the wrong algorithmic complexity only after the code is written, the whole implementation may have to be thrown out; if the core assumption is checked first with samples, properties, or small-scale experiments, the error stays in a smaller scope. Large amounts of documentation do not create guarantees by themselves, and documents and code can drift apart through later changes.

Royce's practical advice comes down to "do it twice": before formal delivery, build an incomplete but runnable experimental system, so that the most dangerous technical and operational assumptions get feedback early. It shares something with today's MVP, since both buy evidence at limited cost; the difference is that an MVP usually validates user needs and product assumptions, while a pilot emphasizes the technical and operational risks of a large system. Agile development breaks this learning into many short cycles, and test-driven development writes the expected behavior down as executable checks in advance.

The classic experience has not become obsolete just because agents now generate the code. Whether a programmer or a model writes it, as long as a system still has to move from an empty project to a deliverable state, it needs to run early, compare results early, and stop unverified assumptions from spreading further downstream.

4. The development process as a data dependency graph

Requirements, design, implementation, tests, and the facts of use after delivery can be seen as interdependent artifacts. Design consumes requirement information, implementation depends on design and specification, testing needs a real implementation, and "whether the result is correct" goes back to the requirements or the specification. User feedback and maintenance records after launch produce new facts that drive the next round of change.

This relationship explains rework better than a stage diagram does. Suppose testing finds an error in payment timeout handling, and tracing upward shows that the design stage never decided the retry semantics, or even that the requirements stage never said whether a timeout might already have charged the account; then the problem sits in no single line of code. Every implementation, test, and document that depends on the old assumption needs another look, and the cost of the change comes from the length and width of the dependency chain.

The MVP method can therefore be understood as controlling the size of the data dependency: rather than finishing a large block of interdependent work that has no runtime evidence, get one highest-value end-to-end path working first, then expand to more features step by step. It does more than trim features: the work turns from covering every layer at once into small closed loops that keep getting feedback. Agile iteration, short release cycles, and TDD all do the same kind of thing, which is to make key assumptions meet evidence earlier.

This view also explains why traceability matters. With no links between requirements, design, code, and tests, a project is just a pile of files; when one business rule changes, no one can reliably tell which interfaces, permission checks, state transitions, and tests need to change with it. Requirement traceability matrices, cross-references, and automated checks are all ways to make explicit the data dependencies that were implicit before.

5. Methods and tools turn vague decisions into checkable objects

Software engineering created different languages and tools for different stages. Requirements documents, use cases, and prototypes help people discuss "what do we actually want"; decision tables, state machines, modules, and interfaces help people discuss "how the system is organized"; programming languages land these decisions as executable behavior. The level of formality rises step by step so that high-risk decisions no longer rest only on memory and guesswork, not so that all work becomes mathematics.

The value of a decision table is that it forces combinations into the open. For example, "students get a discount, and spending enough also gets a discount" still leaves undecided whether the two discounts stack, whether the threshold is computed on the original price or the discounted price, and how to roll back on a refund. The table puts the missed branches in front of people, which gives the discussion something to work on. Checklists work in a similar way: confirming whether the client was consulted, whether a key review was completed, whether acceptance evidence was kept. These look trivial, and they reduce the chance that a whole class of mistakes is missed entirely.

Property-based testing carries this idea into program execution. For a sorting function, requiring only that the output is ordered is not enough, since always returning an empty list satisfies that condition; the output also has to preserve the elements of the input and how many times each one appears. Tools can generate large numbers of inputs from the property and go looking for counterexamples. A passing test only means no violation was observed within the checked range, and it is not the same as a mathematical proof, but each independent quality check increases confidence in the implementation.

These methods share one goal: giving intention a carrier that can be preserved, reviewed, and checked automatically. AI can help generate documents, tests, and models, but if the original requirement was never stated clearly, faster generation only turns different misunderstandings into different code faster.

6. Contracts write the responsibilities between components into interfaces

Design by Contract starts from a simple question: what does the caller promise, and what does the implementer promise? A precondition states what the caller must guarantee before the call, a postcondition states the result the implementer must guarantee once those conditions hold, and a class invariant describes the properties that always hold while an object is in a stable, usable state.

Take an account that does not allow overdrafts. A withdrawal can require the amount to be positive and no larger than the current balance; once it completes, the balance should equal the old balance minus the withdrawal, and the non-negative-balance invariant should still hold. How the balance is stored inside the implementation is not something the caller needs to know; what the caller really depends on is these behavioral promises.

A contract also has to handle the boundary of responsibility. If an insufficient balance counts as an illegal call, it can be excluded from the precondition; if the product requires an insufficient balance to be returned as a normal business result, then a definite failure state should be designed, with the balance specified to stay unchanged. An implementation cannot accept arbitrary requests and then, when the abnormal case happens, dodge the product requirement by saying "the caller should not have called it that way".

Contracts matter more in the AI era. An MVP can leave all the boundaries unhandled for now, but as the system grows, the cases that were ignored enter real use. Only by writing preconditions, postconditions, and invariants down can tests, runtime checks, and formal verification work around the same set of promises. An implementation can be rewritten quickly; the semantics of the interface cannot live only in one chat log.

7. UML and how semantics are preserved

UML tries to provide a shared modeling language between natural language and code, so that requirements and design can be discussed with relatively stable elements, relations, and constraints before they become implementation. Its value lies in the questions it forces: which intentions must be fixed, and which details can stay open for now, rather than in how many diagrams you have memorized.

A few distinctions that diagrams easily hide are worth keeping:

Object to distinguish Software engineering meaning
Model and diagram A diagram is only one view of the model; the absence of a constraint from the diagram does not mean it is absent from the model or has been cancelled
A single execution and a behavioral promise One successful call does not mean all legal inputs, retries, and concurrent cases meet the requirement
Legal, illegal, and unspecified Unspecified behavior does not have business approval by default; it is simply undecided
Access, ownership, and lifecycle Being able to reach one object from another does not mean the two share ownership or are destroyed together
Order and concurrency Vertical position on a diagram does not automatically mean globally serial execution; strict ordering needs an explicit constraint

A payment system illustrates these boundaries well: charging the account after the user confirms and returning success can be a legal trace; charging without confirmation can be an illegal trace; and whether a request that times out after confirmation is retried automatically may still be unspecified. If the implementer chooses automatic retry, they then have to answer whether the charge already went through, how to avoid charging twice, and how concurrent callbacks are handled.

The model itself needs checking too. OCL, metamodels, and other constraint mechanisms can check types, multiplicities, input and output compatibility, and the well-formedness of a model; a model that follows the modeling rules does not automatically satisfy all the business goals. Tools execute written rules reliably, and they do not decide on anyone's behalf whether the rules are written correctly or cover the risks that really matter.

Today AI lowers the cost of generating a program straight from natural language, which weakens part of the reason people once needed UML to carry semantics around. Saying "UML's diagrams are outdated" does not mean the first principles inside it stop working: fixing the semantics that can be fixed, expressing different meanings separately, keeping undecided space explicitly open, and making models, implementations, and evidence traceable to one another are still effective engineering principles.

8. Three kinds of information to preserve in the AI era

When an agent can generate large amounts of implementation in a very short time, the most important engineering interface goes beyond "please write the code" and becomes the boundary of three kinds of information:

  • Behavior that has been decided: user confirmation, state transitions, data constraints, and interface promises;
  • Behavior that is explicitly forbidden: unauthorized charging, access outside the granted permissions, duplicate processing, and unacceptable failure modes;
  • Behavior that is not yet decided: it can stay undecided for now, but it must be marked as pending rather than quietly decided by the implementer.

As long as the first two kinds of promise are met, internal data structures, algorithms, and code organization can keep their implementation freedom. Version control preserves the history of these decisions, requirement tracing connects decisions to implementations and tests, and contracts, properties, state machines, and verifiers let machines keep checking part of them.

The value of software engineering lies in keeping the system working under complex constraints, not in making every programmer hand-write every line of code. AI makes implementation cheaper, and it also makes wrong implementations appear in bulk more easily; the faster code is generated, the more constraints, evidence, and undecided space need to be preserved explicitly. The next stage of software engineering is to organize intention, specification, implementation, and feedback into a closed loop that does not run out of control easily, rather than to race agents over who writes faster.

More articles →

Loading full version…