IEEE Software - The Pragmatic Designer: Stable Code from Stable Problems
This column was published in IEEE Software, The Pragmatic Designer column, May-June 2026, Vol 43, number 3.
ABSTRACT: Lehman’s Laws distinguish between stable S-type and volatile E-type code. Developers can decompose unique problems methodically by seeking standard sub-problems, which reduces complexity and boosts productivity. This illuminates the nature of software engineering, which is needed whether code is written by humans or machines.
Software is written when there’s a novel problem to solve. For example, students write linked list code as an exercise, but industry software developers do not; they reuse the library code. So, how do developers divide a novel problem into many smaller problems, some of which are solved by libraries?
Software developers divide big problems into small problems every day, often without much thought. Unlike algorithms and data structures, this is a skill that tends to be learned through experience, not coursework. Perhaps that’s because we don’t understand it very well. We do have some guidance though.
David Parnas advised us to think of our current software as the precursor to many future systems. [1] We don’t know what the software will be asked to do tomorrow, so we should divide our current system into parts that we might be able to rearrange and reuse in a future system.
Edsger Dijkstra warned us that there are many ways to build something that works, but most of those will be difficult to reason about. The rationale (or in Dijkstra’s case, proof) and the code should “grow hand in hand”. [2] If we want to reassemble several modules into a new system tomorrow, it’s easier if we can understand what each of them does individually, rather than reason about them together. [3]
Like most developers, I’ve read the advice about how to decompose a big problem into smaller ones, and yet in practice, I tend to decompose by gut feel. I’ve recently learned about a technique that makes the choice of decomposition more methodical.
To do this, we must use our experience and ingenuity to recognize stable sub-problems within every unique problem presented to us. That’s because the code for stable sub-problems behaves differently than most code we are familiar with.
This approach differs from typical practice. Few developers look for stable sub-problems, except by intuition. Inexperienced developers, in particular, often take the unique problem they are presented with and decompose it into smaller but still unique sub-problems. It’s custom code all the way down. Beware of this easy path! There are no off-the-shelf solutions to unique problems. When we seek and find standard sub-problems, then we can either reuse an existing library or we can write code to put into a library.
Lehman’s Laws
Perhaps this technique seems obvious but I could not see it until I studied Lehman’s Laws of Software Evolution. [4] Meir Lehman spent his life seeking to describe how large software evolves, much the way the Ideal Gas Law describes how gases behave. When folks like Dijkstra and Hoare were predicting that we’d soon be proving all of our software correct, Lehman was skeptical. [5]
To say that software is proven correct, we need two things: a description of its behavior (i.e., a specification or just “spec”) and a proof that the code conforms to that specification. Lehman’s insight was that not all specs are stable. [6] The code that Dijkstra and Hoare were proving correct, such as sorting algorithms, was anchored to a stable problem, so the spec was stable. It would be very surprising to learn tomorrow that sorting and searching have changed their nature but, in contrast, nobody should be surprised if tax rates change tomorrow. That’s because sorting and searching algorithms are anchored to a stable problem; tax calculations are anchored to an unstable problem.
Lehman called these two kinds of code S-type (“S” for specified) and E-type (“E” for evolutionary).1 To be S-type, code needs a spec and it must be anchored to a stable problem. The critical part isn’t the existence of a spec because with enough effort you can specify anything, even ever-changing tax rates. It’s the anchoring of a spec on a stable or unstable problem that makes it S-type.
Lehman observed that essentially all of the programs we use are E-type because they solve a problem in the world, and since the world isn’t stable, those programs evolve alongside the world to stay relevant. Lehman’s Uncertainty Principle states that “No E-type program can ever be relied upon to be correct” because you could prove it correct today only for the world to change tomorrow, invalidating the spec. [7]
Lehman observed that our E-type programs are composed of code that can be either E-type or S-type. If we are insightful enough, potentially all of an E-type program can be composed of S-type code. Consider a simple shell script that consists exclusively of calls to existing programs. If each of those programs is S-type, then the only E-type code is the script itself.
This is when I had my aha moment. Lehman wrote about programs2 and sub-programs. The S-type / E-type distinction works equally well for any methods, procedures, or functions within a program. I realized that sometimes I’m calling sort() from the standard library – that’s S-type code. Other times, I’m writing custom methods for unique problems – that’s E-type code. Before I call or write any code, I must decompose the bigger problem I’m facing into sub-problems. Often I do this instinctively, without much thought, and I find E-type sub-problems, but the opportunity is there for me to seek out more S-type code.
So the technique boils down to this: decompose problems into stable sub-problems, not into volatile sub-problems dependent on whims of the world, like today’s tax rates.
Finding S-type code
Once I learned this technique, I recognized cases of it from my past where I looked at a unique problem and recognized a stable problem embedded within it. Here’s an example.
I worked with someone who was instinctively better at this decomposition than I was. I’d written some code to extract the top 3 items from a collection, which I’d done with a for loop, based on some custom logic to define what “top” items meant. That is, I’d decomposed an E-type problem into another E-type problem. His review comment to me was: think of this as three problems.
- Figure out a way to compare two items (a comparison function).
- Put the items into an ordered collection, sorted by your comparison function.
- Take the top 3 items from the collection.
At the time, neither of us knew about Lehman’s terminology, but now I’d see the first part (the comparison function) as unique code, so it’s E-type, and the second and third parts as S-type because they’re standard library code.
Programs that solve business or information technology problems won’t be as stable as math libraries, but I’ve found that I can make a problem more stable by defining abstractions. One example is activeCustomer. Even when the world changes and we must redefine what constitutes an active customer, the rest of the code is typically unaffected because it rests on the activeCustomer abstraction. From this, I conclude that Lehman’s sharp distinction between E-type and S-type code should more accurately be seen as a gradient, with some problems more stable than others. Math and informatics problems tend to be the most stable and business problems are less stable.
When I initially decompose a problem, I rarely find all of the opportunities for S-type code. I often decompose naively into E-type code – perhaps because I have so many years of bad habits – and then critically examine what I’ve just done, looking for S-type opportunities. It fits into a virtuous cycle of code improvement that I’ve written about before. [8]
My co-worker had trained himself to find S-type problems embedded in E-type ones. At the time I thought he was applying a functional programming technique but it’s more general than that.
It takes time to train yourself to recognize opportunities for S-type code. I want to emphasize, however, that once you’re able to seek out S-type code, it’s not slower than decomposing into E-type code. In fact, it’s typically faster because you’re reusing more and writing less. That’s how expertise works: experts are faster than novices.
Impact
Why bother with S-type code? I see at least three reasons. The first is that S-type code accumulates differently than E-type code. As we’ve seen, because it solves a stable problem, S-type code may already be in a library, and if not you could add it to an existing library.
The world presents us with an unending series of problems to solve. If we resort to writing custom solutions to each – that is, we write E-type code – then we’ll be accumulating a lot of custom code. Each time we instead recognize a stable problem hidden within what’s presented to us, that’s code we don’t have to write, test, or maintain. If your company finds S-type code more often than your competitors, you will build systems faster than they do.
Consider the three graphs below. These are just cartoons to help you see how S-type code accumulates differently than E-type. Graph A shows the unlikely but theoretically possible situation where you always recognize S-type code, which rarely needs to change. Graph C shows the opposite, the all-too-easy to imagine situation where code is always written from scratch, never reused. Graph B is a mixture.
Notice how the code in Graph A accumulates like pancakes or stratified layers, the code in Graph C like vertical bars, and the code in Graph B in-between as diagonal stripes. As the size of the system grows, the cost of writing and re-writing in (Graph C) grows rapidly, while the cost to recognize and reuse (Graph A) is minimal.

Figure 2. Patterns of code accumulation. These three notional graphs show how long code lives before being rewritten. (A) shows the theoretically possible 100% S-type code. (B) shows a mix of E-type and S-type. (C) shows the all-too-easy to blunder into 100% E-type code. Assuming it’s equally costly to write a line of S-type or E-type code, the total cost to write and maintain the system in (A) is much less than the system in (C) because there’s minimal rewrite or complexity cost in (A).
These three graphs are cartoons but Rich Hickey showed graphs that looked just like A and B. [9] His Graph A was for Clojure, where code that went into its libraries had a brief juvenile period where there was some churn, but after that, it leveled out into horizontal pancakes. Graph B was for another language. A strategy of seeking S-type code is possible and pays off with low code churn.
The second impact of seeking S-type code is reduced complexity. As Rob Pike wrote:
“Organic growth is not simple; it generates fantastic complexity. Each piece, each change may be simple, but put together the complexity becomes overwhelming. Complexity is multiplicative. In a system, like Google, that is assembled from components, every time you make one part more complex, some of the added complexity is reflected in the other components. It’s complexity runaway.” [10]
Each of those unique E-type parts you build will have wrinkles custom to the unique problem. As you compose them into a system, those wrinkles multiply and you slow down.
The final impact of seeking S-type code is that it can help steer a project to health. Project leads will have developed a gut feel for good and bad code. They can’t transfer that gut feel to their teammates, but they can teach others to seek out S-type code. They can track the ratio of E-type to S-type code on the project: the ES-Ratio. The ES-Ratio acts like a compass indicating health trends on the project.
The nature of software engineering
The nature of software engineering doesn’t change; it’s the same discipline whether a human or artificial intelligence (AI) types the code. Its nature has been obscured because, since the beginning, developers and academics have disagreed about the effectiveness of various software engineering advice and heuristics.
Empirical validation has been elusive because it’s difficult to create identical conditions between experimental groups and, if you could do so, it’s wildly expensive to build full-scale software multiple times. Now that AI is writing code, it’s easy to recreate identical conditions and cheap to build software. As a result, we may learn more about the nature of software engineering in the next five years than we have in the past fifty.
Some people’s first thought is that since AI will be writing code, it’s less important to understand software engineering. In fact, the opposite is true. Regardless of who writes the code, the world runs on software so it matters how it is written. A better understanding of software engineering leads to better code: higher quality, easier to write, and less expensive.
Understanding the role of E-type and S-type code in software engineering is a piece of the puzzle. Mary Shaw has observed that by understanding software design, we enable anyone to do what formerly required a virtuoso. Soon, we’ll expect that from AI.
References
- D. L. Parnas, “On the Design and Development of Program Families,” IEEE Trans. Softw. Eng., vol. SE-2, no. 1, pp. 1-9, Mar. 1976.
- E. W. Dijkstra, “The Humble Programmer,” Commun. ACM, vol. 15, no. 10, pp. 859-866, Oct. 1972.
- E. W. Dijkstra, “On the Role of Scientific Thought,” in Selected Writings on Computing: A Personal Perspective. New York, NY, USA: Springer-Verlag, 1982, pp. 60-66.
- M. M. Lehman, “Programs, Life Cycles, and Laws of Software Evolution,” Proc. IEEE, vol. 68, no. 9, pp. 1060-1076, Sept. 1980.
- M. M. Lehman, interview by W. Aspray, IEEE History Center, Sept. 23, 1993. [Online]. Available: https://ethw.org/Oral-History:Meir_Lehman.
- M. M. Lehman, “Laws of Software Evolution Revisited,” in Software Process Technology (EWSPT 1996) (Lecture Notes in Computer Science, vol. 1149), C. Montangero, Ed. Berlin, Heidelberg: Springer, 1996, pp. 108-124.
- I. Herraiz, D. Rodriguez, G. Robles, and J. M. Gonzalez-Barahona, “The Evolution of the Laws of Software Evolution: A Discussion Based on a Systematic Literature Review,” ACM Comput. Surv., vol. 46, no. 2, pp. 1-28, Dec. 2013.
- G. Fairbanks, “Fix Tech Debt With Virtuous Cycles,” IEEE Softw., vol. 40, no. 2, pp. 111-116, Mar./Apr. 2023, doi: 10.1109/MS.2022.3228623.
- R. Hickey, “A History of Clojure,” Proc. ACM Program. Lang., vol. 4, no. HOPL, pp. 1-46, June 2020.
- R. Pike, “Simplicity,” Command Center, Dec. 2023. [Online]. Available: https://commandcenter.blogspot.com/2023/12/simplicity.html (accessed Nov. 30, 2025).
