You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

16KB

Context Engineering — Chapter-17: Modularization

  • Source: /library/Context Engineering/source-file.pdf
  • PDF pages: 118–129
  • Pages without text: none

Modularization The previous chapter ended with a caveat: folders communicate where each feature lives, but they do not stop visitors. Watch the caveat turn into a bill. Weeks after VilaSchedule was reorganized, Vila Nova Clinic’s clinical coordinator asks for a refinement to appointment scheduling: if the provider’s day already has a work-in, an extra appointment squeezed into a full schedule, the front desk should see a warning before confirming, because a day with a work-in is a tight day. You hand the task to the agent, and the session is a pleasure to watch. The directory listing points to the appointments slice, the agent opens appointment_orchestrator.ts , needs to know whether the day has a work-in and finds the answer on the other side of the tree: the day’s work-in count lives in day_limits.ts , inside workins . It imports the file directly, calls the count, shows the warning. Nothing complains. The tests pass, the review passes, the front desk says thank you. The bill arrives on a Friday, in what looks like the most self- contained task in the book: the work-in limit rule changes internally, so that it now subtracts the day’s schedule blocks before counting openings. This is one slice’s business, and the agent works the way chapter 13 promised: it opens workins , rewrites the count in day_limits.ts , adjusts the slice’s tests. Except that the old count had a second client nobody remembered: the appointment orchestrator, hanging off that import from the week before. The front desk warning breaks in a flow the task never mentioned, the agent has to load all of appointments to understand the damage, and the session that was one slice becomes two, with the tokens of chapter 6 and the distractors of

chapter 5 billed twice. Notice the mechanism: the feature tree said where each topic lives, and it told the truth. What it never said is what, inside each slice, the neighbors are allowed to touch. Without that second piece of information, every file is a front door by default, and the context of any change is, in the worst case, the whole system. Parnas’s criterion The diagnosis is fifty years old. In 1972, David Parnas published “On the Criteria To Be Used in Decomposing Systems into Modules” in Communications of the ACM (DOI 10.1145/361598.361623), comparing two divisions of the same program. The first divided by processing flow, one step per module, which is everybody’s instinct. The second divided by what he called information hiding: each module hides a design decision that is likely to change, and exposes to its neighbors a surface that survives the change of that decision. In his words, each module “is characterized by its knowledge of a design decision which it hides from all others.” The paper’s conclusion: when the hidden decision changes, only the module that hides it is touched; in the division by flow, the same change cuts across nearly all of them. Parnas calls that surface an interface, and I will avoid the word for the rest of the chapter. It has two senses today that fight each other: his, which is the set of what a module publishes to its neighbors, and your language’s, which is the interface keyword of TypeScript, Java, C# or Dart. Two sections ahead, I show why the confusion is expensive. Only the first sense matters here, and I call it the public surface.

Translate that into Friday’s terms. “How a day’s work-in opening is counted” is exactly a decision that is likely to change, and it changed. If workins were a module in Parnas’s sense, that decision would sit behind a stable surface, something like “does this provider’s day accept a work-in?,” and the appointment orchestrator would depend on the question, not on the machinery of the answer. The rule would change inside the slice, the question would stay the same, and the front desk warning would never find out. The direct import of day_limits.ts did the opposite: it coupled a neighbor to the machinery, and from that point on the decision stopped being hidden, with the blast radius of every internal change stretched as far as the import reached. Here is where this book’s reading comes in, the one Parnas had no way of offering in 1972. What a module hides is, by definition, what whoever is outside it does not need to know. For a developer, “does not need to know” saves reading; for the agent of chapter 3, which is born with no memory and assembles the window from scratch in every session, “does not need to know” saves load. A respected module boundary is an implicit reading instruction: from this slice, read only the public surface. The interior is context the outside agent never pays for, in tokens or in attention. Information hiding was invented to limit what a human had to understand before making a change; in 2026, it limits what a session has to load before acting, which is the same principle billed in a new currency. Deep modules, small surface What makes a public surface good still needs saying, because hiding everything behind any old surface solves nothing. John Ousterhout, in A Philosophy of Software Design (2018), gives you the yardstick with a geometric image: think of the module as a rectangle whose width is what the caller has to know, and whose

height is the functionality, what the module does for whoever calls it. The good module is deep: a lot of functionality behind a narrow surface. The bad module is shallow: what it publishes is nearly the size of the implementation, and the caller learns almost as much as they would learn doing the work by hand. His classic examples are the Unix file operations, half a dozen calls hiding decades of different file systems. Ousterhout’s yardstick and Part I’s arithmetic are the same account written twice. The width of the rectangle is, literally, the context a client of the module loads: a narrow surface enters the window in a few lines; a wide surface drags signature after signature into the session. And the depth is how much system the agent moves through without reading: every call to a deep module is functionality obtained at no cost to the window. A shallow module is the worst of both worlds for a session, because the agent reads the whole surface and still has to peek at the implementation, since what was published does not support a line of reasoning on its own. If chapter 12 measured the persistent artifact in help per token, the yardstick for a module is the same: functionality per token of surface. In VilaSchedule, the question “does the day accept a work-in?” is a deep surface: one operation, two arguments, and behind it the daily limit, the subtraction of blocks and whatever else the rule picks up later. Publishing all of day_limits.ts is the shallow alternative: the caller knows the count, its format, the order of the checks, and all of that knowledge turns into coupling that some future Friday charges for. A public surface is not the interface keyword

Time to make good on the promise from two sections back, because this is where many projects read Parnas and produce the opposite of what he proposed. Publishing a narrow surface does not mean creating an abstract type for every part of the system. They are independent things: the surface is the set of what the slice lets the neighbor call, and the language’s interface is a polymorphism mechanism, which exists to swap one implementation for another at run time. My own FOCUS Architecture (https://books.kodel.com.br/en/books/focus/) is explicit on this point, and it is worth citing because that book’s rule governs the structure this chapter is fencing. It catalogs the “ceremonial layer” as an antipattern: an IOrderService with exactly one implementation, a data transfer object (DTO) identical to the model and a mapper that copies field by field hide no decision at all; they only charge a toll. That book’s yardstick is Mark Seemann’s, in Dependency Injection in .NET (2011, second edition in 2019): you extract the abstraction when the second real implementation shows up, not preemptively, just in case. The exception FOCUS grants is the repository, where the second implementation exists from the first week, because the in- memory test double implements the same contract as the repository that talks to the database. Two real implementations are architecture; one implementation and a name with an I in front of it are bureaucracy. For an AI session, the cost of that bureaucracy is chapter 5’s cost, measured in files. Every abstract type with no second implementation is one more file the agent’s search finds, one more symbol it has to disambiguate and one more hop between a declaration and code that actually runs. The agent that goes looking for “where the daily limit is counted” and lands on an empty declaration spends tokens to discover that it has to go

looking again. The public surface this chapter defends is the opposite of that: not one extra file in the path to the rule, only a list of who has permission to leave the slice. Boundary lines across VilaSchedule’s tree None of this requires throwing away chapter 13’s structure; it requires promoting it. Look again at the slice that caused the incident, now with the one file this chapter adds: ├── workins │ ├── day_limits.ts │ ├── index.ts │ ├── workin.ts │ ├── workin_controller.ts │ ├── workin_orchestrator.ts │ └── workin_repository.ts As a folder, this slice made everything public by default. As a module, the slice uses index.ts to declare what goes out and hide the rest: // Front door of the workins feature: create a work-in and answer whether // the provider's day still accepts one. How the opening is counted stays // in day_limits, which is internal and does not leave this folder. export { WorkInOrchestrator } from “./workin_orchestrator”; export { WorkIn } from “./workin”;

Five lines, and notice what they do to Friday. The work-in orchestrator is the door, in the sense FOCUS already gave it: a slice talks to a slice through the orchestrator, never through an internal file. The Parnas decision hidden in there is “how the opening is counted,” which lives in day_limits.ts and is now absent from the list of exports, free to change without telling anyone. workin_repository.ts disappears along with it, for the same reason. The same exercise runs through the other slices: scheduling hides how intervals are generated from the weekly schedule and publishes the availability query; appointments hides the WhatsApp confirmation flow and publishes the operation that books an appointment. The map of who may depend on whom ends up like this, with the import from the start of the chapter marked as the edge the boundary forbids:

An index, though, is an invitation, not a fence: nothing stops the next agent from writing the deep import all over again. That is why the boundary needs a second line, one a tool enforces. In VilaSchedule, where every slice is reachable through the @features prefix, the rule fits in a lint configuration file: { “rules”: { “no-restricted-imports”: [ “error”,

{ “patterns”: [ { “group”: ["@features//"], “message”: “Another feature only through its index.” }, { “group”: ["../../*"], “message”: “An import that climbs two levels leaves the feature.” } ] } ] } }

The first pattern allows @features/workins , which is the index, and blocks @features/workins/day_limits , which is the interior. The second closes the back door, the relative path that climbs two levels and comes down inside the neighboring slice. Inside the slice itself nothing changes: the view keeps importing ../orchestrator , one step sideways, and neither pattern matches that. The shapes age with the language, so I record the 2026 ones as instances and not as a recipe: besides the index with lint, there is the monorepo in which each slice is a package and declares what it exports, and there are languages where visibility belongs to the compiler, like Rust’s modules or Go’s packages. Two properties do not age. First, the boundary has to be verifiable by a tool: a boundary that lives in a team agreement repeats the fate of chapter 11’s implicit convention, and the agent, which was not in the agreement, violates it in the first session. Second, it has to be readable in the listing: index.ts at the root of the slice appears in the exact place where every session begins, and the agent that lists workins sees right away the file that says what in there is for external use. Notice what this does to the artifacts this whole part has been building. Each slice’s boundary is a convention, in the sense of chapter 11, and verifiable like the best ones there. The choice of what scheduling hides is a decision with alternatives and consequences, and the why behind it fits in an architecture decision record (ADR) from chapter 10. And the compound effect shows up in the window: with boundary lines enforced, the context of a task in appointments is the appointments slice plus the index of the slices it depends on, a few lines each. Without them, it is the slice plus any file some import has already reached, a set that only grows. Chapter 13’s structure tells the agent where to start reading; this chapter’s boundary tells it where it has permission to stop.

“Too much ceremony for a system this size” The objection you will hear: VilaSchedule has five slices, everybody knows what is internal to each one, and an index plus a lint rule are the kind of ceremony only a large system needs. The short answer is that “everybody knows” describes today’s team and leaves out the contributor that produces the most code on the project, the one that rereads everything from scratch in every session and treats as public everything it manages to import. That is how the import at the start of the chapter came about: the agent did what the structure allowed. An explicit boundary replaces a team memory with a fact about the repository, and a fact about the repository is the only thing the agent sees with any guarantee. Notice the size of the bill, too: five index files of three to five lines and one lint rule, with no new abstract type, which keeps the boundary standing without falling back into the ceremony FOCUS condemns. The opposite objection deserves a record as well, because Ousterhout makes it against his own remedy: dividing too much is a disease with a name in his book, classitis, the proliferation of shallow modules, each of which does so little that the complexity spills into the connections between them. For an AI session, classitis is a specific poison: a system of forty shallow modules serves the agent forty surfaces in the window and no deep functionality behind them, chapter 5’s catalog of distractors with an architect’s signature. The yardstick is still depth, not quantity: VilaSchedule’s five slices become five modules, and the right modularization here is to draw five boundary lines, not to create the sixth. With that, Part II closes the circuit for the project you control: the spec for the intent, the living doc for the present, the ADR for the why, the conventions for the how, the persistent file for what every session sees, a structure that screams the features and

boundary lines that limit what each task loads. Reread that list with a suspicious eye and you will notice the premise hidden in every chapter: somebody, at some point, got the chance to do it right early. Most of the code in the world got no such chance. The system you inherit on Monday is eight years old, has no spec, has a docs/ folder with one file from 2019, decisions that live in the memory of people who have left and a structure nobody chose, which just happened. Handing that system to an agent with no context at all is a recipe for Part I’s hallucinations; writing the whole quartet before touching it is a quarter of a year nobody is going to give you. There is a middle path, which extracts context from what the legacy system already offers for free, from the cheapest signal to the most expensive. That is the topic of the next chapter.

Powered by TurnKey Linux.