Don’t accrete, unfold

Complexity is the main antagonist in your developer life story. Avoiding it should be one of your highest goals. Apart from embracing the ideals of simpler code, you must also remember the value of preventing unnecessary complexity introduced by integration.

The development of software is generally a case of adding to an existing codebase by introducing new code through alteration or integrating something new. The unfolding process asks us to take a sequence of steps, always maintaining a successful system at each step. This is at odds with integrating anything from outside.

The alternative strategy of adding complexity and trying to pay it down later only works if there is a later. Accretion leads, inevitably, to highly complex, difficult-to-maintain code. So, we should use unfolding sequences or admit we intend to increase complexity and are willing to pay the price. This is one interpretation of technical debt, pushing the cost into the future.

Growth through differentiation

But how can you develop software so that all steps are complete? Complete steps are when we apply a pattern of differentiation of the space or identify an issue and repair it into a better state. One way is to follow the advice of Mel Conway and make software a thing that is running and malleable all the while. This represents a similar situation to the way Christopher Alexander built his buildings. We take a look at where we are and see what needs to happen next. And then, we do it right there with our hands. Very agile. Working with suitable materials is a prerequisite. If the materials don’t allow you to make complete steps, the materials may be at fault, or it could be that you need to make different changes before you can make the necessary changes. We should invoke the wisdom of Kent Beck’s famous tweet, ‘for each desired change, make the change easy (warning: this may be hard), then make the easy change’. In other words, relentlessly refactor.

But what of integration? Integration isn’t a form of differentiation; it’s a form of including something from outside. Was this seen before in Christopher Alexander’s work? Perhaps. Including some outside construct to help solve a problem could be seen as using bricks or plumbing. The whole is concerned with the larger structure. Integration of utility methods and capability libraries is easy to resolve because while they are complicated materials, they are materials out of which we assemble the greater whole.

But then there are the more turbulent integration tasks, such as when a whole subsystem must be introduced to resolve a particularly gnarly problem. Then, it’s more than just complex material. It’s a joining of forces under a new whole. In this case, the only realised example I can think of is not a project by Christopher Alexander; it’s the Citibank building.

The building has a novel four-stilt construction because it had to share the land with a church, even though the bank was permitted to use the airspace above it. This peculiar contract first led to a single hollowed-out corner solution, but then the decision was made to hollow out all the corners, building the whole structure on stilts and a central column. But, even though the structure was sound according to all standard regulations, the novel design introduced non-standard problems. The stilts were in the middle of the faces of the building, so in effect, they created a footprint of a smaller building at 45 degrees, which could have led to catastrophic failure1. This is true of any codebase that has to contort itself to a new configuration to adapt to the incoming large change. Tests that previously sufficed to give strong indicators of correctness may no longer cover all the aspects of the system as a whole, and emergent and unwanted behaviour can manifest.

Is there a safe way to integrate large changes? Maybe not. However, being aware of the difference between integrating plumbing and integrating an entirely new way of handling water and gas is enough to raise the sensitivity to potential problems. We become aware that it’s not ‘what we always see’. Our tests breed a form of complacency. When introducing something larger, we must be alert to their inadequacy.

Stresses and tensions can guide our unfolding and differentiation, but each act should maintain wholeness. Just as massive integrations cause issues, disintegration can also cause problems. Dangling effects such as unused features or libraries are stressors, so they should be culled. If we keep them because we think we might need them later, then we should be aware that this is not strategic thinking but loss aversion.

What is unfolded?

Behaviour is unfolded. Structure can be unfolded too. But the materials themselves are not. They most often emerge from an unfolding, being required to perform it. We introduce new materials to differentiate or increase the quality of an existing element. You don’t unfold the design of a pipe, but you can unfold how the pipework connects to all the other pieces of the puzzle. And you can unfold the rooms’ design using that pipework pattern language. Unfolding is about taking something that is already complete and taking it to the next step. A staple, a nail, or a brick is not a construction. This is why they don’t unfold. They are what we use to complete the unfolding process.

Unfolding will always be of something that is already at least somewhat complete. Something whole and extendable based on a revelation of what is missing. Something that, even if utterly incapable of solving the problem it was intended to solve, is at least in a reasonable position to begin to solve it. A ‘Hello, world!’ program is whole with respect to almost all possible applications. From that small starting point, a direction is apparent, and unfolding can take us along the many steps to a batch processing application, a web service, an IDE, or even a computer game. Each step is small, but you can always get there from here. Each step is growth and adaptation.

Optimisations can also be unfolding operations. Consider deduplication of code increasing clarity. We can adjust a now obvious spatial problem to include a partitioning container. We can review our code for anything we deem inconvenient or sub-optimal at any step and make that next step one in which our actions are optimising space, time, clarity, safety, or security. Not every unfolding has to be towards a customer-visible goal.

What doesn’t count as a step?

Copy-pasting code into an existing program usually brings with it some external context. That context may bring unexpected complexity or resolution to problems of which you are unaware. This quiet resolution is not entirely healthy. You won’t know what to fear. The specific decisions made when writing the copied code are lost in their static nature. Copying code gives you a solution but not wisdom, and not always resolution.

In addition, not quite finished code is not a step. Doing some work and adding it to the codebase only makes it a step once it is live. If some intermediate code is important, then there are two options I can think of. Either you put it in a new branch as an incomplete step, or you use what you have to make a step, even if the change is not a direct improvement. It’s okay to have it make things the same. It’s just not okay to make things worse. And untested, unused code is worse.

Feature flags create a strange world where the unfinished lives beside the working code. Disabled code causes stresses in the working code by its presence but adds no direct value. I liken this to setting up a temporary kitchen in your living room with a plug-in stove, microwave, and kettle while refitting your regular kitchen. It’s a stressful time where nothing works quite as well as it should, but there’s hope of a brighter future.

Can the unfolding process resolve this? How do you do a bit of code surgery on a sizeable living codebase when you need to replace a whole system or way of working? It’s the problem of repair, not unfolding. Sometimes, repair feels different because something is in pain during the operation of restoration. Does this help us? Recognising a code change as a repair rather than typical development allows us to give it the space and respect it deserves. It does not help make it safe, but it does suggest we put up warning signs and hazard tape around the site of the wound and work. And that is what the feature flags do for us. By their presence, they indicate a thing in flux and inhibit accidental regression by compiling the new and old code in the same space.

So, repairs are not always steps of unfolding. Sometimes, they are surgery, and to do surgery, you often need a scalpel, and the patient will need time to heal.

Final form

The final form of an unfolding process is beautiful because it is an unfolded form, not because it is final. The consciously made decisions producing the form provide its beauty. The lack of debris from process accidents makes it beautiful through a sense of effortlessness and cleanliness. When the form is considered complete or in a state of satisfactory equilibrium, it is not beautiful because it is static. Often, we give up on what we could have had by striving to achieve strictly what we thought we could get. Unfolding guides us to what we could have so long as we keep our eyes open, often revealing a superior and beautiful final form.

1

Many of the details here are paraphrased from the Wikipedia article https://en.wikipedia.org/wiki/Citicorp_Center_engineering_crisis or from the ‘99% invisible’ podcast https://99percentinvisible.org/episode/structural-integrity/