Course page: Requirements and Architecture (2) · Slides · Video

1. What space requirements and architecture each shrink
A system can implement today's requirements completely and still bog down at the next modification. Working only answers the current question; architecture has to answer two others: which decisions can change independently, and where a new requirement can land. If one feature change means editing many pages, database fields, and synchronization scripts at the same time, the system's complexity has already exceeded the complexity of any single feature.
From structured programming and growable languages to UNIX, relational databases, and the Internet, many classic designs do something similar: they offer a small set of stable concepts and collaboration rules, so that more uses can be created on top of them. Business systems need the same ability. Especially when AI can produce implementations quickly, stating facts, states, and computation dependencies clearly does more to determine whether a system can keep evolving than having the model patch results over and over.
Between human intention, a system's specification, and the actual implementation there is always more than one possibility. Making course selection convenient for students might use first-come-first-served, or preference ranking, or a lottery; even with the business rules settled, there remain different choices for data structures, module boundaries, page interactions, and how the thing runs. Requirements engineering confirms step by step what is really needed and reduces ambiguity in the intention; architecture fixes responsibilities, interfaces, and constraints, and reduces the room for arbitrary combinations between specification and implementation.
These choices face two different kinds of complexity. Essential complexity comes from the problem domain: courses have capacity, students have to satisfy prerequisites, classrooms and times conflict, and after a change of major the courses already taken still have to be recognized. Hand all of that to a registrar working with pen and paper and the constraints do not go away. Accidental complexity comes from the implementation: the same information stored in several places, formats converted repeatedly between modules, one rule scattered across many pages, caches and databases each holding a result that can be modified. The business never asked for that work; it is the burden of how the software is organized.
Architecture can model eligibility, capacity, and scheduling separately, so each part can be understood on its own and then cooperate through interfaces. It cannot make prerequisites disappear, and it can still let student enrollment, administrator adjustments, and bulk import share one set of rules. Databases provide a similar isolation: an application can rely on transactions and a storage interface, leaving part of atomicity, durability, and concurrency control to a specialized system rather than reimplementing them in every business feature.
Any isolation brings trade-offs, though. Plain files are easy to inspect and process with command-line tools, while an update spanning several files does not automatically get the guarantee of a database transaction; a database offers stronger management and also requires the application to follow a query interface and runtime constraints. Choosing a technology means trading one kind of complexity for another, and a tool being "more advanced" is not enough to make it right for every task.
Login is especially good at exposing this chain of consequences. A username and password look like two input boxes, and unfolding them brings up registration, password confirmation, forgotten passwords, identity recovery, and sending email or text messages. Third-party login adds integration conditions, service dependencies, and cost. A vague "login is needed" already contains many decisions that affect later maintenance; having the model implement it directly just leaves those decisions to be guessed.
So MVP work goes depth-first while thinking about requirements goes breadth-first, and the two should support each other. Walk one critical path through, get runnable feedback, and examine a few alternatives with far-reaching effects at the same time. Longer requirements are not better: every commitment has to be reviewed and maintained, and in an AI workflow it also consumes context and increases the chance of missing a constraint. What counts is finding the decisions that must be explicit, and knowing which assumptions can still be adjusted.
2. Layering begins by respecting the limits of human cognition
Dahl, Dijkstra, and Hoare's Structured Programming discusses many programs that look small today, and it raises a question that has not aged: people can hold only a limited number of elements in mind at once, so how can that limited capacity build and check a larger system? Piling every detail into one function asks a person to re-understand the whole problem each time.
Enumeration, induction, and abstraction each help in a different way. Enumeration checks finite branches, induction helps with loops and recursion, and abstraction lets a person depend only on the effect an operation promises. With a queue, you can discuss enqueue and dequeue first without also thinking about array indices, memory allocation, and grow strategies; in understanding a business, you can discuss enrollment eligibility first without unfolding database access at the same time.
The "pearl necklace" metaphor covers more than cutting code into pieces. Each pearl can carry one design decision, and the dependencies between decisions determine which parts can change independently. Fix the image as a row-by-row representation early and many intermediate layers may be forced to understand "rows"; postpone that decision and more layers can keep operating on the concept of an "image". The further an implementation detail spreads, the more parts move when you replace it.
Classes and objects wrap data and operations into one concept, while coroutines help separate control flow. A subroutine organizes work around call and return; a coroutine can pause, keep its local state and execution position, and resume where it left off. When two chess programs take turns, each waits for the other and each continues its own computation. Making one of them an ordinary subroutine of the other may break the original structure; a coroutine expresses this peer cooperation more directly. Its value lies both in reducing thread-switching cost and in letting different activities keep their own processes.
Debugging dynamic programming has the same architectural problem. When the code comes down to dp[i][j][k] and layers of loops, the business or mathematical meaning of the states is usually crushed out, and one wrong transition can silently produce a wrong result. Re-expressing state names, reachability relations, and where each transition comes from, and even printing or visualizing a state graph, lets you check the program at a level that is easier to understand. Once it is verified, rewriting it into an equivalent compact implementation gives efficiency and comprehensibility a shared basis.
From linked-list operations to discrete event simulation, and on to orders and machines on a shop floor, concepts can be built up layer by layer: the bottom layer manages connections, the middle layer manages simulation time and waiting, and the top layer describes requesting resources, processing, and releasing them. Each layer added gives users vocabulary closer to their problem and lets them forget a batch of details that have already been handled. That is the practical payoff of layering.
3. Growable designs provide primitives and ways to combine them
Guy L. Steele Jr.'s Growing a Language presents language growth through a constrained talk: it starts from monosyllabic words, and a complex word can only be used after it has been defined or after an established rule of word formation produces it. The audience therefore experiences the path from a small set of primitives to complex expression. A language with very few words is easy to grasp and makes anything slightly complex hard to express; a large language stuffed with every feature at once can be hard to implement, learn, and deliver.
Programming languages face the same trade-off. A sufficiently small language does not mean actual programs are simple. C's generic sorting interface needs an element count, an element size, and a comparison function, and callers have to honor the type and memory conventions themselves; when certain abstraction mechanisms are missing, the complexity moves into the user's code. Conversely, an extension mechanism that only produces hard-to-understand tricks can make both language and programs harder to maintain.
Growable design asks whether later users can build new concepts, whether those concepts compose, and whether the composition still follows clear rules. A designer does not have to list every future use in advance, but must provide primitives, interfaces, and extension mechanisms sufficient to construct them. Language users can write new libraries, system users can add new tools, and business software can take new rules or implementations inside its existing boundaries.
Architecture like this leaves the future a set of constrained choices. Which collaboration rules must stay stable, which implementations can be replaced, and which empty slots later people will fill are all part of the current design. It asks for some foresight while avoiding a complex mechanism built in advance for every imagined requirement. A very practical check: when one requirement changes, how many places have to change with it?
Classic texts are where these judgments come from. Reading them is worth it for understanding why an abstraction appeared, what burden it removed, and what limits it kept. AI can help retrieve, explain, and connect existing knowledge, but the specific mechanisms and examples in the original text are still what you check against. Handing a book title to a model in exchange for a paragraph of "classic" summary does not replace understanding the design decisions.
4. UNIX turns a shared interface into a capacity for combination
UNIX philosophy emphasizes doing one thing well per program, letting programs cooperate, and using the text stream as a universal interface. The third point is what makes the first two work in practice: output can be read by a person, saved, or handed to another tool, and the intermediate results of a composition can be inspected at any time. Users do not have to ask a tool's author to implement the whole workflow in advance.
A pipe is only the connection mechanism; reliable composition also depends on shared conventions. How input and output are organized, which channel diagnostics take, how arguments are interpreted, and how exit status represents a result all affect whether tools can cooperate. Standard output carries data and standard error carries diagnostics, which keeps error messages from mixing into the records the next step has to parse. Text needs a definite structure too; when file names can contain newlines, "one file per line" is not enough to pass a list without ambiguity.
A tool's capability can be reinterpreted inside a composition. jq extracts a JSON field, fzf takes candidates and lets a person choose, the editor opens the selection, and you have a file-opening flow that matches your own habits. Each component still handles its own local task, and human judgment can be one step in it. A shared interface lets users create uses its designer never imagined.
This is a concrete form of growable architecture. Authors provide tools and the rules for their cooperation, and users take charge of new combinations; a component's internal implementation can change while its external protocol protects the rest. Compared with stuffing every capability into one enormous program, this structure makes it easier to replace a part, observe a failure, and reuse existing work. The premise is that participants keep respecting the interface, and do not casually treat a progress bar or a display format as machine-readable data output.
5. How databases and the Internet accommodate change
A relational database separates the logical meaning of data from its physical organization. A library needs to express readers, holdings, and lending relations, and an application can query "which books has a given person borrowed" or "who has borrowed a given copy" from the same set of relations. Which data pages the records actually sit on, which index is used, and which table is scanned first do not have to be something every application feature understands.
SQL describes the result that is needed, the query optimizer chooses the execution plan, and transactions organize the reads and writes that have to hold together as a whole. On top of this layer of abstraction, a great many systems organize their features around create, read, update, and delete, that is, CRUD. A database does not list in advance every problem a school, a shop, or a library will have; it provides the expressive power of relations and queries, so each application can define its own schema and ask new questions.
The Internet offers a similar openness. Applications depend on a communication interface, and the transport, network, and link layers each handle their own problems; an application has no need to understand the implementation of every physical link along the way. TCP provides an ordered byte stream and the reliable transport mechanisms that go with it, and connection failure still has to be handled; higher layers can add security protocols and new business protocols without cramming every kind of use into the base network.
RFC 1958, discussing Internet architecture, stresses continuing technical change. The specific ways of doing remote login, file transfer, and network services will evolve, and the basic communication capability still has to leave room for different applications. Stable collaboration rules allow parts to be replaced, so the system as a whole keeps working.
UNIX, databases, and the Internet each fix part of the rules for tool exchange, logical data, and communication cooperation, and each leaves uses, storage implementations, and upper-layer applications to later people. Their shared contribution is a general enough layer of abstraction on which more systems can be constructed. Business architecture has to decide further how a particular client's complex rules sit on top of that infrastructure.
6. MVC gives each kind of change a home
An early web information system can treat each page as a piece of database work: receive the request, query data, check conditions, write records, and return HTML. Template languages lowered the cost of organizing pages, and they can make prompts in an AI application easier to maintain; putting variables into an explicit template stays closer to what you finally want to express than concatenating a mass of strings and conditions.
Code embedded directly in a page also means business logic gets put there casually. The student enrollment page checks capacity once, the administrator's adjustment page checks it again, and the import script copies it once more. When the rule changes later, every entry point has to be found. Switching to tags or templates does not automatically produce separation of responsibilities.
MVC, that is, Model-View-Controller, separates data and business state, presentation, and the handling of user input. The Model encapsulates courses, enrollment records, and the rules; the View presents results; the Controller receives input and coordinates business operations. The Model carries business meaning and cannot be understood as database tables alone; the Controller should not become the place where all rules finally converge.
Boundaries like these give each change a home. Turning the timetable from a list into a weekly calendar mainly affects the View; changing course capacity and prerequisite rules mainly affects the Model; adding an administrator entry point for adjustments should reuse the existing business capability while expressing the extra authorization condition. A maintainer can work at one layer instead of understanding pages, queries, and every rule at the same time.
MVC and CRUD suit a batch of local information-management tasks well, but they only move business complexity to a clearer position. Splitting one directory into three names does not by itself make the business simpler; what does the work is interfaces, rule reuse, and clear responsibility boundaries for data modification.
7. Interface state needs a model too
The course list in the business database stays the same, the keyword in the search box changes, and the courses shown on the page change with it. An expanded menu, a selected row, temporary input, and hover state are all information that determines the interface. These states usually do not need to be written back to the business database, and they still should not scatter across a heap of controls and event callbacks.
The interface can be summarized as View = render(Model, ViewState): the business model and the interface state together decide what should be shown at this moment. With state and rendering separated, a maintainer can check the input state first and then the display computation. Whether local state needs to be persisted and whether it deserves to be modeled explicitly are two separate questions.
MVVM goes further, using a ViewModel to organize the state and behavior that presentation needs. Take course filtering: the course data and the filter term are inputs, and the visible course list is a computed result. Vue can hold the filter term in reactive state and use computed to express how the list depends on it. v-model wraps the two connections, state determining the control's value and input events updating the state, and the framework then updates the display according to the dependencies.
React expresses similar relations through components, state, and explicit updates. A component describes the interface from its current props and state, an event requests a state update, and the next render uses the new state snapshot. Since the filtered list can be obtained from the courses and the keyword, there is no need to store it separately as its own State, which would mean maintaining consistency between two copies of the data.
The two ways of expressing this give different programming experiences and share the goal of writing the dependencies down clearly. The framework does the corresponding updating; the application states which states are sources and how results are produced. The virtual DOM, dependency tracking, and update strategies can hide part of the implementation complexity, but they cannot judge for the application whether a business rule is correct, and they cannot turn every UI state automatically into a pure computation with no side effects.
8. Long processes expose the limits of the current snapshot
An order may pass through placement, inventory lock, discount calculation, payment, risk control, shipment, refund, after-sales service, and reconciliation. A failure at one step affects the actions that already happened before it and the actions allowed after it; a manual exception approval can also bypass the normal path. A registrar system runs into long-term connections between grades, credits, the graduation project, asset return, and special policies.
Representing these processes with one status code or a set of independently editable fields easily crushes different histories into the same "now". Risk control failing but being approved to continue and risk control never being triggered may both show as the next stage; paying and then refunding and never having paid may leave the same net balance. Keep only the current result and the system loses the basis for understanding the difference.
State also often mixes in expectations about the future: awaiting payment means something has not happened yet, and accounts receivable means a right or expectation that still needs handling, which cannot simply be treated as cash already received. When things do not go as expected, overwriting the current field can make the history that already happened disappear along with it.
Problems like these belong to the complexity of real business. Putting all the code in a Model, or drawing a bigger state machine, only changes where something is expressed. Architecture has to distinguish facts, interpretations of facts, and expectations, and state the dependencies among them.
9. Event Sourcing computes reports out of facts
Double-entry bookkeeping offers an example of this structure. One transaction produces matching records in the accounts involved, total debits equal total credits, and the relation among assets, liabilities, and owner's equity can also be checked. It adds a way of finding errors, and a balance does not mean the transaction was classified correctly; entering something twice or posting an amount to the wrong account can keep the books balanced too.
A bookkeeping tool can combine Event Sourcing with LLM as a Compiler: keep the original vouchers and the transactions that occurred, let the language model propose constrained bookkeeping actions, and then have a program with definite meaning compute how those actions affect the ledger and the reports. The model handles human material, the program applies the rules, and a checking step verifies that each action corresponds to the real transaction.
This architecture keeps the model from guessing every cell of the balance sheet, the income statement, and the cash flow statement separately. The related figures in several reports come from the same set of actions and their computation rules, and which items an action affects has a definite meaning. Recognizing an expense, creating a payable, and actually paying can be expressed separately, so a payment does not count an expense a second time after it has already been recognized.
Event Sourcing separates past, present, and future: the past keeps the facts that have happened, the present view comes from those facts through rules, and expectations about the future can be corrected by later facts. Mistakes, corrections, voiding, and reversals also need a record, so that a change in the current result can be explained. Keeping history does not require a mistake to stand forever; it keeps a new result from being explained by quietly deleting history.
Replay provides the ability to regenerate views and leaves room for scenario exploration. Without changing the real ledger, hypothetical events can be used to compare the effect of cases such as receivables that cannot be collected. This is computation based on explicit assumptions, and it does not automatically yield a reliable probability of risk; a forecast still depends on whether the inputs and the model are reasonable.
Recomputation also needs time boundaries for rules and external inputs. Reproducing a past report should use the rules and exchange rates that applied then; re-evaluating under new rules has to state which facts those rules apply to. Replay of internal computation must not execute external actions such as payments a second time. Restoring state and changing the external world have to be kept apart, or one repair can create a new business event.
10. Turning a synchronization problem into a checkable computation
In a registrar system, the grade for one course affects the set of passed courses, and passed courses in turn affect credits, prerequisite eligibility, and graduation conditions. If all these results are stored as independent facts and allowed to be edited directly, a grade correction forces every consequence to be updated by hand. Miss one and the system holds mutually contradictory conclusions at the same time.
A clearer representation keeps courses, grades, recognition records, and the applicable rules, and treats completed courses, credits, and exit conclusions as derived results. Changing a course from passed to not passed produces a correction and recomputes the affected relations rather than flipping one mark. Curricula and course credits can change over time, so the computation must know which version to use, and cannot substitute today's data for the basis that applied then.
Exception approvals belong in the system of facts as well. Course substitution, condition waivers, and manual approval should record the object, the scope of application, and the basis, and later rules then use them. Setting "can graduate" to true leaves only a result; at the next review or recomputation the system still cannot explain the decision. Meeting the course conditions and meeting all graduation requirements also need to stay clearly distinct.
Data dependencies have to be traceable. Every important result should be able to answer: which facts were used, under which rules, through which computation? The computation can be rerun, tested, and audited, and once dependencies are explicit it is easier to judge which conclusions a correction affects. A program can still contain mistakes and results can still lag, but the basis for verification and repair has been kept.
SQL can express queries, and a database offers views, constraints, and materialized views; the difficulty usually appears between the database, the application logic, the cache, and the interface. Once a base record changes, arbitrary computation relations that cross these layers are not automatically maintained as one. A total-credits field that has lost its source and its way of being rebuilt can disagree with the real course records even inside a transactional database.
MVVM, React, Dataflow, and Event Sourcing make derived state and dependencies more explicit at different layers. They are not the same technology and cannot replace one another directly, and they can be understood under one design question: which information must be stored as facts, and which information should be computed from them? Keeping indexes, summaries, or caches for performance is fine, as long as the source, the invalidation condition, and the way to rebuild are known.
The complexity of software shifts accordingly. Maintaining many independently editable states requires a programmer to keep remembering the synchronization relations; keeping the necessary facts and expressing the computation concentrates more responsibility on whether the computation is correct. This problem more readily yields reviewable inputs, outputs, and evidence. For generative software engineering, making these objects explicit also gives AI a shared basis for generation, modification, and checking.
Good architecture therefore needs room for combination and extension, and needs results to have a provenance. Abstraction limits what has to be understood at one time, interfaces assign responsibility for change, and facts and rules support rebuilding results. Code can be generated faster, and a system still has to explain why it is in its current state and which decisions need re-examination when the next requirement change arrives.
