Parnas Was Right. It Is Still Hard.
Everyone can draw the flowchart. The hard part is drawing what changes together.
One company I worked at divided the system by pages of the website. The search page by search team, the detail page by the detail page team etc. Shared business logic was known by both teams. Are they covering this rule or are we, oh ok. But we both need to know it. At this small company it was not ideal but not big of a problem. Try that at a larger company with tens of teams…
At another company the team I was part of had to maintain a component that collected all the data that was entering the system boundaries, normalize it and feed it into the system. Every team came back to us saying their feature doesn’t work on the test environment, for which we had to look time and again whether the service we maintained did its job right. I ended up making a dashboard for everyone to check first before coming to us, which clearly showed whether the data was handled and published into the internal system. That finally took some of the burden off of our team. Nevertheless, our sprints were still disrupted by change requests we had to review and test on this service because they realized the change was needed there in the middle of their sprint and the feature they needed was “urgent”, so we had to collaborate.
The list goes on with places where modularization was seen as impossible. “Everything is tightly connected here, you cannot possibly split our system into separate modules?!” So they have been working on a large monolith with this belief for years, not able to delegate work between separate teams.
The pain stems from how hard it is to modularize software systems in a way that enables parallel work, while each business rule is owned (and known) by one team only.
So, I have been thinking about how to modularize software systems for nearly two decades. I have done it across monoliths, across microservices, across legacy rewrites and greenfield builds for domains ranging from pension, housing, banking, fraud detection and energy. I have read the foundational papers, internalized the principles, attempted writing my own tooling for it. I have given the same advice on whiteboards more times than I can count.
And here is the strange thing I have come to believe. Modularization done well is not just hard. It is cognitively unnatural. It works against the grain of how we think of systems. Most of the time, when teams produce worse modularization for software (there is no good or bad in software engineering, there is only better or worse), it is not because they don’t know better. It is because the alternative is harder to think about, and under any kind of pressure, our brains slide toward what is easier.
The Frame I Have Found Most Useful
There is a 1972 paper by David Parnas, On the Criteria To Be Used in Decomposing Systems into Modules, that gave me the basis for what I had already been struggling with. It is short, and the argument is simple enough to understand. However, applying what he suggests to real world systems has been difficult for me.
Parnas takes one small program and splits it two ways. The first split follows the steps of the processing, the way you would draw a flowchart. The second follows a different rule: list the design decisions that are likely to change, and give each one to a module that hides it from all the others. The interface of a module exposes what is stable. Its implementation hides what is volatile.
This is the principle of information hiding.
By the way this term has been misunderstood and stretched to death. It is everywhere. It has completely lost the point that Parnas made. People use this term to mean any kind of abstraction, while Parnas introduced the term to make his point of modularizing systems based on what is likely to change, rather than to decompose them based on steps in the process.
The principle is easier to state than to apply. So I start with an analogy from a factory, and then work through Parnas’s example step by step.
Each example follows the same steps:
- What the system does.
- Decomposition 1: split by steps.
- The decisions likely to change.
- Decomposition 2: each decision goes to one module only.
- A change test: which modules change in each decomposition.
A note on scope. In these examples I only talk about how to split the concepts. Whether the resulting modules live inside one monolith or run as separate, autonomous services is out of scope for this post. So is how they communicate, for example REST calls or messaging. Where a diagram shows one module notifying another, read it as “this information flows here”, not as a choice of transport. The goal here is narrower: to learn to think in terms of hiding decisions likely to change, instead of thinking in steps.
A shampoo factory
Picture a production line that fills shampoo bottles, caps them, labels them, packs them into cartons and stacks the cartons on pallets. The obvious design is one robot per step. The filler knows this bottle’s height and this formula’s thickness. The labeler knows this label panel and this artwork. The packer knows this bottle’s diameter, and that six go in a carton.
Now marketing launches a new curved bottle. Every robot needs work.
Now design the same line by asking what is likely to change. Bottle shape and sizes change all the time. They will invent new recipes of shampoo over and over again too, what are they still inventing there I don’t know. Different colors and words to stand out at the shelve and sell more. Maybe a new way of sticking the label onto bottles to save glue needs to be instructed to the robot that sticks the labels onto the bottles. They might try to improve the way they detect defects.
In almost all of these examples, the change requires updating multiple robots, if we design the robots according to steps of the process. New recipe of shampoo, the shampoo becomes heavier than it used to be because of this change now the bottle needs to change to be smaller or thinner, the boxes also change to hold the new bottle, the label needs to change to list the ingredients.
It is interesting that information hiding is applied to physical factories today. Instead of this modularization based on steps in the process, they already modularized the factory based on what is likely to change and what changes together.
Firstly, they decided to invent the “puck”. Each bottle rides in a plastic cup. Every puck has the same outside shape. The pocket inside fits the bottle and puts every neck at the same height. So the robots see one kind of container, whatever bottle is inside.
| Robot | Before: it knows | After: it is told | |
|---|---|---|---|
| Filler | This bottle, this formula, 400 ml | Fill this volume at this flow | |
| Capper | This neck, this cap | Apply this cap at this torque | |
| Labeler | This label panel, this artwork | Apply this label at this position | |
| Packer | This bottle’s diameter, 6 per carton | Pack this many into this carton |
Now the curved bottle arrives again. Write a new recipe. Order new pucks. No robot changes.
This is not only a thought experiment. The ISA-88 standard for production control separates product recipes from what the equipment can do, and pucks are common on lines that run odd-shaped containers, such as cosmetics.
Keep three things from this picture: the steps stayed, each robot’s interface changed, and a new module appeared that no flowchart would show. Parnas’s own example does exactly the same.
In software, the same rule can give a different shape. When a value changes together with the rule that uses it, such as a price and its discount rules, the value lives with that rule.
Decisions likely to change
| # | Decision | Example change | Kind |
|---|---|---|---|
| 1 | Bottle shape and size | Marketing wants a new curved bottle | Product |
| 2 | What goes in the bottle | A new, thicker formula | Product |
| 3 | What the label says | A new country, a new language, new legal text | Product |
| 4 | How many bottles go in a pack, and the pack material | A retailer wants packs of four | Product |
| 5 | How pallets are built and marked | A new pallet type, a new carrier | Product |
| 6 | What counts as a good bottle | A tighter fill tolerance | Product |
| 7 | How bottles move through the line | Guide rails replaced by pucks | Equipment |
| 8 | How the filler doses | Piston replaced by flow meter | Equipment |
| 9 | How caps are applied | A new capping head | Equipment |
| 10 | How labels are applied | Wrap-around replaced by front and back labels | Equipment |
| 11 | How bottles are boxed | A new pick-and-place arm | Equipment |
| 12 | How cartons are stacked | A new palletizer | Equipment |
| 13 | How defects are found | A camera instead of a scale | Equipment |
| 14 | Which vendor builds each machine | Replace the capper with another brand | Equipment |
Rows 1 to 6 are product decisions. They change whenever the product range changes. Rows 7 to 14 are equipment decisions. They change only when the line itself is upgraded.
Decomposition 2 split by decisions likely to change
| Module | Hides | Interface |
|---|---|---|
| Product recipes | 1 to 6: which products exist, and everything about each one | Named parameters for the current product |
| Container handling | 7: how bottles move, and the bottle’s shape from everyone else | Every bottle rides in the same kind of puck |
| Filling robot | 8: how it doses | Fill a given volume at a given flow |
| Capping robot | 9: how it caps | Apply a given cap at a given torque |
| Labeling robot | 10: how it labels | Apply a given label at a given position |
| Packing robot | 11: how it boxes | Pack a given count into a given carton |
| Palletizing robot | 12: how it stacks | Stack in a given pattern |
| Inspection | 13: how defects are found | Pass or fail against given tolerances |
| Line control | Sequence and speed | Run a product |
The robots receive what they need as named parameters: a volume, a torque, a label image. They never know which product they are making.
Change test
| Change | Decomposition 1 | Decomposition 2 |
|---|---|---|
| New product: curved bottle, new scent, new label | Every robot | A new recipe and a new puck insert. No robot changes |
| New country | Labeling robot | The recipe |
| Packs of four | Packing robot, palletizing robot | The recipe |
| Tighter fill tolerance | Filling robot’s own check | The recipe |
| Two products on the same day | Reprogram and retool every robot | Load the other recipe, swap the puck inserts |
| Filler switches from piston to flow meter | Filling robot, and every product setting inside it must be entered again | Filling robot only |
| Capper from another vendor | Capper, its product settings, its neighbors’ signals | The capper only |
| A product needs a pump cap the capper cannot fit | Capping robot | Capping robot, plus the recipe |
I really like this example because I think looking at a physical world example explains the point better and out of the box. Here, six product decisions share one box: the bottle, the formula, the label, the pack, the pallet and the tolerances. At first this feels wrong. In software, we are trained to distrust one module that holds everything. But look at what changes together. Product values change with other product values: a new product often brings a new bottle, a new formula and a new label at once. They rarely change together with a robot’s logic. The filling robot doses the same way, whatever the formula. So the values belong together, and apart from the robots.
What this example teaches
Flowchart thinking groups things by temporal coupling (what happens at the same time or in the same pipeline step), whereas information hiding groups things by change vector alignment (what changes for the same business reason).
By decomposing systems according to the information hiding theory, we contain the changes to least amount of modules at a time as the system evolves.
Volatility is not about predicting exact future features; it is about recognizing the axes of variability already baked into the domain rules or product boundaries (e.g., we don’t know what the new bottle shape will look like, but we know bottle shapes will change).
Seeing the physical factory settings also came to adopt the same idea illustrates perfectly how the information hiding theory is the core principle we should try to modularize software systems accordingly.
Parnas’s KWIC index
The original paper Parnas wrote where he introduced the term “information hiding” and explained the effect of modularization based on decisions likely to change rather than the steps in the process, can be a bit daunting to read. I wanted to explain it here so that we can look at the original example.
The system
A KWIC (keyword in context) index takes lines of text. For each line, it makes every circular shift: it moves the first word to the end, again and again, until every word has been at the front. Then it prints all shifts of all lines in alphabetical order. That way you can look up any word and see it in its context.
Here is an example of the input and output of the KWIC system. For the input lines “modules hide decisions” and “change is likely”, the output is:
change is likely
decisions modules hide
hide decisions modules
is likely change
likely change is
modules hide decisions
Decomposition 1: by processing steps
Five modules, one per box in the flowchart:
- Input reads the lines and stores them in memory. It packs four characters into each machine word and keeps an index of where each line starts. (“Packing” means storing several characters in one memory slot to save memory. Computers store data in fixed-size units called machine words. Memory was scarce in 1972. So instead of using one machine word per character, the Input module put four characters into each one. It also used a special unused character to mark where each word of the text ends. )
- Circular shift builds an index of all shifts. Each entry is a pair: the original line number and the address where the shift starts.
- Alphabetizing reads the arrays of Input and Circular shift, and writes a sorted index in the same pair format.
- Output reads the sorted index and the lines, and prints them.
- Master control runs the other four in order.
The interfaces between these modules are memory layouts. Every module must know how the lines are packed and what the index looks like. Parnas notes that this is roughly the decomposition most programmers would propose.
Decisions likely to change
| # | Decision | Why it might change |
|---|---|---|
| 1 | The input format | A new input source |
| 2 | All lines are kept in memory | Large jobs do not fit |
| 3 | Four characters are packed per word | Small jobs run faster with one character per word, or another packing is needed |
| 4 | Shifts are kept as an index | Write them out in full, or compute each one on demand |
| 5 | Everything is sorted once, up front | Search per item, or sort only as much as needed |
Decomposition 2 split by decisions likely to change.
| Module | Hides | Interface |
|---|---|---|
| Line storage | 2 and 3: where and how lines are stored | CHAR(r,w,c), SETCHAR(r,w,c,d), WORDS(r), plus counts and deletes |
| Input | 1: the input format | Reads lines, calls SETCHAR |
| Circular shifter | 4: how shifts are kept | CSSETUP, CSCHAR(l,w,c) |
| Alphabetizer | 5: when and how sorting happens | ALPH, ITH(i) |
| Output | No decision from the list | Prints lines or shifts |
| Master control | No decision from the list | Runs the others |
CSCHAR looks just like CHAR, and that is the point. The circular shifter pretends to be a line store that holds all the shifts. Callers cannot tell whether the shifts exist in memory or are computed when asked.
There is no shared data store anymore. The lines live inside Line storage, and nobody else knows how.
Change test
| Change | Decomposition 1 | Decomposition 2 |
|---|---|---|
| 1. New input format | Input only | Input only |
| 2. Lines no longer all in memory | Every module | Line storage only |
| 3. Different packing | Every module | Line storage only |
| 4. Shifts kept differently | Circular shift, Alphabetizing, Output | Circular shifter only |
| 5. Sort lazily | Hard: Output expects a finished index | Alphabetizer only |
What this example teaches us
By splitting/modularizing the logic in pieces/modules according to decisions that are likely to change, we reduced the number of pieces/modules we have to touch when those changes are needed. Since systems change all the time, and we are trying to optimize our software systems to be changeable, modularizing according to the criteria of what is likely to change makes better sense than to modularize by steps in the process.
Why It Is Hard To Think This Way
Looking back at the factory and at KWIC, decomposition 1 was the one you could draw by thinking of steps in the process or the workflow. Decomposition 2 needed knowledge that was not in the system. It required thinking about what marketing will ask for, what law will change, what a retailer will want etc. That gap is I think the difficulty.
The artifact biases us toward steps
Imperative code lures us into procedural thinking. We read control flow top to bottom. Stack traces are linear, and tests run in the order of setup then action then assertion. Unless we are actively designing reactive or event driven systems, our default representation of a running system is procedural, do this, then do that.
Thinking about what will change together lives in business roadmaps, domain rules, regulatory shifts, and the heads of people with the domain expertise. Step based decomposition is the path of least resistance because it mirrors the execution flow that is in front of our eyes. Change based decomposition forces us to bring context in from the outside.
Steps ask “what happens?”; modules ask “what’s volatile?”
Decomposing by steps asks: what does this system do right now? Decomposing by information hiding asks: which decisions we are making today are volatile?
I did confuse the question of what is volatile with which changes I can predict. But I don’t think it is about predicting the future but it is about to finding out the design decisions we already know are volatile and wrap them in interfaces so their inevitable changes don’t leak everywhere.
The feedback comes too late
A worse split does not hurt when you ship it. The system works. The pain comes months or years later, when one change touches seven modules instead of one. Even then, we tend to take this as normal in software engineering practice. For us to realize there is a better way to modularize takes years of seeing different systems evolve and extra effort to want to improve.
Step based decomposition is System 1; change based is System 2
Following data through a system is quite automatic. You see the architecture, you trace the flow, the boxes draw themselves. It does not feel effortful.
Finding the decisions likely to change is slow. For each decision, you ask how sure you are, what would make it change, and who would ask for that change. Then you check which other decisions would happen together. It is tiring, and it is hard to keep up for a whole design session. The asymmetry is brutal. The worse way is cheap. The better way is expensive. Under any kind of load (fatigue, a deadline, an unfamiliar domain, a meeting full of strong opinions) we slide toward the cheap option.
System 1 and system 2 are terms used by Kahneman in his ground breaking book, Thinking fast and slow, where he argues how our brain works in two modes.
In the system modularization space, your brain defaults to System 1 (flowcharts) because code syntax is inherently linear, meaning fighting for change based modularization requires deliberate, exhausting System 2 exertion.
Put these together. The code pushes us toward steps. The better split needs knowledge that is not in the code. And the thinking is expensive.
Back to the real world pain I talked about
Looking back at those page based teams stepping on each other’s toes, or the ingestion service flooded by urgent feature requests, I realize why we struggled to find the right split. We were trying to separate data that naturally wanted to connect. But Parnas reminds us that modularization isn’t about finding where data is isolated. It’s about hiding how decisions are implemented behind stable interfaces. When the database schema drives your architecture, everything is coupled. When volatility and business lifecycles drive it, you can finally build boundaries that let teams move fast without stepping on each other. It’s hard, it goes against how our brains naturally trace execution flows, but getting it right is the difference between a system that scales and a monolith that grinds to a halt.
When colleagues look at a relational database, drawing a flowchart or writing a quick SQL join across tables is a System 1 breeze. Proposing an abstract boundary that hides the data structure and forces teams to communicate through controlled domain interfaces feels like “unnecessary overhead” until a requirement changes and breaks half the application. I couldn’t convince them easily because information hiding requires upfront cognitive friction to save pain down the road, and engineering culture under delivery pressure always defaults to the path of least resistance.