We Don’t Program Anymore, We Manage Programming (AI impressions, part 3 of 5)

This is the third part of the series “Impressions of Our Current AI Usage”, as outlined by the introduction article.

In the early years of my career, there was a sudden increase in books about management in the technological fields, especially software development. I didn’t make the connection then, but it was right around the time when the first generation of academically educated software developers were senior enough to climb the corporate ladder one step further and become middle managers. Authors like Tom DeMarco and Timothy Lister wrote books that we discussed fiercly, because their propositions clashed with our observed reality of commercial software development.

“Why are our managers so bad at their job?” we asked. Don’t they see the obvious problems that they themselves created? What we didn’t get was that those managers were very experienced developers, but absolute beginners in their new job – leading people that develop instead of developing things themselves. They knew exactly how they would solve the technical problems, but were forced to concentrate on the very untechnical problems of people under their supervision. Nothing in their previous career prepared them for the challenges that “dealing with people problems” would impose upon them.

I personally know several senior developers that turned down (or reverted) a promotion to a management position, because they might gain some money and status, but lost all the things they were passionate about and good at. Working hard to create the environment where others can have the fun you would want to have yourself, is not the most rewarding job in the world. I fully understand them. And I say that as the founder and boss of a software development company. I’ve found my balance between being a manager and a developer, but it’s a constant negotiation.

The premise of our job as software developer was that customers tell us what they want and we describe it to the computer. In essence, we were translators between the worlds of humans and machines, right down to the languages: Some languages for human interaction, others for machine interaction. Because the computer was dumb as a rock, we needed to craft the solution with immaculate precision and in great detail. If anybody asks why software development took so long and was so demanding, imagine explaining quantum field theory to a first grader, so that the child can work in a laboratory among experienced physicists without screwing everything up.

And now, there is artificial intelligence (currently in the form of LLM-based agents) that produces working source code if prompted roughly right. Back in my days, we would call this role a “junior developer”. Which transforms our role to that of a manager: We bring the requirements from the customer, sketch a solution with our senior developers and architects (in the best case, this is also us) and explain it in necessary detail to the junior developers. Then we wait for them to translate our vision for the computer, review the result and iterate as long as necessary.

The only advantage of this scenario in comparison to the same role a decade ago is that the current junior developers aren’t human anymore. So while we need all the skills a capable manager should have, we don’t have to deal with different personalities and human problems in the workplace.
Except that LLMs tend to exhibit the worst work personality types (charming overconfident dazzlers) and are as ruthless and negligent as human junior developers in their “early ability – peak stupidity” phase. As their manager, you can work with human junior developers on their character journey, using the tools from the “people problems” part of your role, while you can only hope for the next version of LLMs to be more pleasant to deal with.
So, as middle managers between the customer and the juniors, we don’t have to deal with “soft topics” anymore. But we have to endure their effects (good and bad) daily. We are “promoted” to be highly ineffectual managers that supervise a bunch of knowledgable, but otherwise clueless juniors. That’s more or less the experience of previous senior developers that became managers and found themselves overwhelmed and underjoyed. The problem is that our new machine-based junior developers are so fast with their work that we can’t replace them with humans and expect similar performance. And the classic “instruct, oversee, review”-cycle that middle managers go through again and again is sped up from days or weeks to minutes. Which leaves virtually no leeway for typical management activities that still run on human speed and require time.

My hope is that managing AI agents requires a totally different skillset than managing human junior developers. Because otherwise, I can attest most of us experienced developers a pronounced inability and unwillingness to be effective in this new role. And I don’t want to imagine a world where most software development is done by poorly managed AI agents while their managers act and feel like fish out of the water.

AI Code Won’t Be for Humans Much Longer (AI impressions, part 2 of 5)

This is the second part of the series “Impressions of Our Current AI Usage”, as outlined by the introduction article.

In the first years of software development, the word “source code” didn’t exist, because code was just that: encoded machine instructions. How they were encoded changed rapidely, from flipping bits in the RAM directly by mechanical switches over feeding paper tapes with punched holes to magnetic storage. But for a long time, we worked with none or little abstraction over the actual machine code. I remember assembler code listings that had two columns of text: the first column for the computer, the second one just translating the first column into human-readable text.
And even with this little bit of clarification what the code actually does, we already needed additional software that took our source code and translated it for the machine.
With the adoption of higher-level programming languages, the additional software stack grew in depth until the distance between the source code and the actual machine instructions was big enough to warrant an intermediate layer of representation. Programming languages like Java or C# put a “byte-code layer” between our textual source code and the binary machine code. The machine we program against is no longer a real computer, but a “virtual machine” or in better words, a model of a machine. As long as we write source code that works correct with the model, we can assume that all the translation layers will find a way to run it correctly on the real computing substrate.
We are used to this kind of programming. We describe our goals using the machine model and a sophisticated machinery of software and hardware parts make it happen.

Forward to today and we use artificial intelligence (or inference using another kind of “model”) to produce source code in our favorite programming languages by describing our goals in even broader terms than before. We might mistake our prompts for natural language and think that we are able to produce source code by just saying what we want.

The question that poses itself nearly instantly is: If we invented a device that transforms natural language into machine behaviour from scratch today, would we include all the intermediate layers into its inner works? Is it really a good idea to transform natural language into higher-level source code, compile the source code into byte code and do all the weird magic to come up with a sequence of machine instructions that resemble the byte code? Isn’t it more efficient to teach the inference how the actual machine works and let it program directly?
Or, asking from the other side, who is the target audience of the generated source code if nobody reads it and the compiler only parses it once? Why does the inference invent all the variable and method names when the compiler throws them away again in the first step of its processing? Of course, right now the inference only imitates our way of working. But we work like we do because we write source code for other humans. If the human at the helm can’t read any layer of code anyway, why not jump directly to the most obscure representation of code and skip all the readability requirements?

As soon as the inference doesn’t imitate but actually learns about the target computing substrate, it will produce working code that is undecipherable for human readers, but optimal for the machine. (If you want to experience this effect in a tiny dose, I encourage you to play the “TIS-100” programming game). And because most inference users don’t need the readable code anyway, they won’t miss anything and get faster solutions with less hassle.

So my guess is that today’s source code will be a dying art, invented for humans and ignored by the machines because it doesn’t provide anything useful for them. The source code of the future will be less readable, more enigmatic and probably more efficient for the machine. Which means that human intervention or even just participation in the software production process will become more cumbersome and therefore even more limited.

My sorrow is that this distancing of the programming process from actual human oversight might provide a hard depedency on inference work alone. It would mean that humans aren’t just scales slower than the inference, but actually incapable of doing its work by hand anymore.

Computing gets fuzzy again (AI impressions, part 1 of 5)

This is the first part of the series “Impressions of Our Current AI Usage”, as outlined by the introduction article.

In the early days of computing, the mechanism that actually works on the data often was an analog technical device that had a certain kind of fuzzyness to it. Think about paper tape with punched holes as longterm storage: If the paper feeder was not aligned with the distance between the holes, there might be spurious variations in the code. Or, a real possibility from my own childhood: You could store digital data on music tape, an inherently analog storage medium. If the loading process succeeded relied on a mixture of patience, delicate handling, room temperature and luck. Most computing devices had specific analog/digital conversion gateways for the periphery, for the display (a very mechanical cathode ray monitor) and even for their own calculation units. The foundation we built our digital world on was influenced by sunshine, moisture, electrical isolation and lots of other factors that could influence the results. I remember a story about the early mainframe computers where a specific bug only appeared if somebody stepped on the physical floor tile where the cables ran beneath. The pressure change altered the physical properties of the cables which resulted in transmission errors.
Over the years, the physical aspects of computing slowly went away or at least faded into the background. We no longer joked about “cosmic ray errors” because the computing substrate was reliable enough to produce the same result regardless of environmental influences. The world got repeatable and therefore, predictable. We got comfortable with machines that were dumb, but reliable. If they had learnt a functionality, they could repeat it virtually forever, without the slightest variation. We had the precision of a nanosecond clockwork and the determinism of a written story that plays out the same every time it is read.

In the early 1990s, there was the first attempt to soften this black-or-white logic fabric up again. The term “fuzzy logic” was all the hype for a few years. Products like cameras, coffee machines, toasters and even water boilers were marketed as “enhanced by fuzzy logic”. How exactly the coffee got better by miniscule variations in the production process was up to your imagination. The core belief of fuzzy logic was that if we express a formula or algorithm by categorized terms instead of numbers, we could bridge the gap between “gut feeling” and digital mathematics.

In my opinion, the same thing happens again with artificial intelligence as the fuzzy component. I doubt that it disappears as thoroughly as fuzzy logic vanished, but the core belief seems to be the same. If you describe a problem in layman’s terms to an “inference”, it finds a solution that appears to be acceptable. If you describe the same problem again tomorrow, the solution might vary in detail or even in grand concept. What works today might not work anymore tomorrow or work even better. The quality of results rely on “the environment” again, not only on the input. The operating units of computing cease to be deterministic again. Computing gets fuzzy once again.

There are some immediate problems that I see with this approach:

  1. Every quality promise comes with severe limitations: The machine will work as expected today, but it is unclear if that extends very far into the future. The current results vary a bit, but might vary tremendously going forward. If the inference unit isn’t included into the product, it might not work anymore soon. Or it works noticeably different from now.
  2. The machine might change its personality on a whim. This is a problem already with encompassing updates every now and then. My smartphone itself stays the same, but the graphical presentation, usage paths and functionality changes over the course of months, if not weeks. In a world where we are used that a stone acts like a stone, a kitchen timer stays a kitchen timer and a text editor doesn’t turn into an e-mail client, we begin to lose that certainty. Our digital assistants begin to have “phases” with decreased alignment to our use cases. Or, expressed as a positive, we can hope that our digital assistants get to know us better and tune themselves in to us.
  3. We enter a world with limited transferability. One benefit of strict specifications is interchangeability. If you change one capacitor in music electronics, the sound changes (or so they claim). If you change one transistor in a digital circuit, the result stays exactly the same, because the change doesn’t cause enough variance to toggle from “black” to “white”. If the building blocks if your system are less specified digital entities like inference providers, you can’t exchange one against another without possibly altering the system’s behaviour in a noticeable way. This makes the reproducability of an equal system with slightly different components more of an adventure. You just don’t know beforehands that it will work.

There are probably more problems and maybe a lot more advantages to this approach than I can fit into one blog post. My main point is that we layered a strict, digital computing substrate on a messy, analog electronics layer and now put another layer of blurry looseness on top of it. Building future systems on this level might feel like engineering the analog systems of the past. I find it interesting (and ironic) that we try this approach right the moment when the last analog technology heroes step back and take their expertise with them.

Impressions of Our Current AI Usage (part 0 of 5)

There is a lot of hype, noise, love and opinion about the use of artificial intelligence (in all its different forms) in software development. Of course, similar disturbances happen in other markets and academic fields at the same time, but I’m not qualified enough to participate in discussions there.

I feel confident enough to share my impressions on our current usage patterns of AI here. You probably recognize the amount of limitations I put into my statement. The usage patterns evolve quick and still quite radical. I’m no “AI native”, so all I say are just impressions from a certain distance. But I felt confident enough in software engineering for at least 25 years to teach it to the next generations of developers. So I know where we were when it all started.

My impressions will be described in detail in five blog posts, each discussing one specific topic. This is the starting post that introduces the headlines of the following articles, but won’t detail them. If you want to react and comment on a topic, please attach it to the matching blog post so we can keep the discussion on point. I invite you to think along, starting with the headline statements. My thoughts are worthless without your thoughts enriching them with your knowledge and experience.

Let’s have a look at the five impressions:

  1. Computing gets fuzzy again. The components of software systems were never sharply defined, but with AI they tend to act like analog components, having bad days and noisy episodes and all.
  2. Source code gets obscure again. As soon as the AI surpasses the imitation stage of human-written code, we won’t be able to read the generated source code anymore – if the AI bothers to generate source code at all and doesn’t leap to machine code directly.
  3. Software developers don’t create software anymore, they manage and lead software creators. This was the fate of the “senior developer promoted to middle management” all the time, but at least it was humans to lead and manage and not a people-pleasing machine.
  4. We delegate the scalable and fun part of our work to AI. The infamous “10X developer” is now a “1000X AI developer”, but the tedious rest of the work (that exists and makes all the long-term difference) is still up to humans.
  5. The means of production are centralized again. Software development was a profession with incredibly low entry bar (a notebook and a coffee). The actual difference was the skill of the human that did the (mostly intellectual) work. If we all use the same AI (created and provided by infrastructure no single person could just copy), the skill difference will be much smaller and we tend to be interchangeable “workers”.

I don’t expect you to understand my thoughts by just two sentences alone, so stay tuned for the elaborate explanation in the topic-based blog posts of this series.

Topic 1 and 2 focus on technical aspects of software development. Topics 3 and 4 have the remaining human developer in mind, gauging her or his well-being and the skills needed to master a normal workday. Topic 5 broadens the view to economic and even political implications of the changes.

Topic 5 is where I might be wrong the most, because I lack the experience of living through severe changes on the scale that my anticipated changes operate on. I’m not a historian, so I’ll talk about things I only have wikipedia-level knowledge about. I hope that the thoughts are still useful and somebody can provide more content on the topic.

The topic-based blog entries are published in the next weeks or months. I will link them in the list above as soon as they are online. I really appreciate your thoughts, in the form of your own blog entry or a comment.

What Happens When We Don’t Listen to the Whole Album Anymore?

I have lectured university students on software engineering for 25 years now. There are some things that changed over time, some for the better, some for worse. But one aspect worries me: The rise of buffet-style knowledge.

Let me explain what I mean by that term: In one of his books, the legendary physicist Richard Feynman describes a group of highly educated students that could recite every law of physics and all the details of materials, but were unable to act on this knowledge by combining some facts to come up with a solution to a common real-world problem. They ingested all the data, but didn’t digest it. It never amalgamated into a box of mental tools that could be applied to a problem just by thought experiment.

I recognize this pattern in my students, too. One example was working with a protocol that sends characters over a (physical!) wire. Each command was prefixed with an exclamation mark, followed by the mnemonic (an odd word, meaning a garbled mess of characters without innate meaning) and then the line ending. A typical specification for a command looked like this:

! QUIT <CR> <LF>

We approached the implementation by writing tests first, and sure enough, half the students asserted for the existence of a literal “<CR><LF>” at the end of the line. Not the two characters “Carriage Return” and “Line Feed”, but the eight characters as seen. When I asked them if they know about character encodings and the ASCII code, they felt well versed in both topics.

After we combined their tests with the real client implementation, they saw the failed assertions, but couldn’t see their mistake. The real client was lacking the latter half of the command line in their mind. They were amazed when they discovered that there are characters that you just cannot see right away.

They studied all the characters that they saw and just assumed that was all there is. The simple question “how does a text editor know when a line of text is over?” perplexed them. They just never stopped to think about how this thing actually works.

My theory about the origin of this symptom is double tracked: Richard Feynman argued that the type of knowledge tests that the students have to endure is the root cause. My sample size is rather small, but I can see that being a big influence. If the tests ask for connections between different pools of knowledge, the students are forced to link their knowledge. Those students that are unable to digest the knowledge until it becomes a mental tool instead of just a reproducible fact tend to perish. If a test just asks for the reproduction of one topic, the digestion part of learning is an optional bonus on top of the study requirements.

Returning to our example above: If I ask for the reproduction of unit tests and another question about character encodings, both questions can be answered without knowledge about control characters (not visible, but still present).

If I combine both questions and ask for a correct assertion about the length of the quit command (7 characters), I can test who is able to write unit tests and who doesn’t know about control characters and asserts for 13 characters. This type of questions (that requires knowledge transfer or fusion from several topics at once) is actively discouraged in today’s exams.

But the second track of my theory is about the means of modern knowledge consumption. We don’t eat full knowledge meals anymore, we pick the flashy bits and skip the rest. If we could learn by just listening to music, we would skip three songs, fast forward the fourth to the exciting part and then ignore the rest of the album. Compare that to the days of linear music storage, you were heavily nudged to listen to the whole album front to back. And while listening to the “other” songs, two things could happen that are missing from the picky approach: We had time to appreciate the exiting part even more and we could be surprised by a song that might be even better than the one we anticipated. Our music portfolio was not only curated by us, but by the artist, too.

Transfer this to software engineering and my grief can be retyped into: Nobody reads whole books about a software topic anymore. In fact, I had several students acting aghast when I suggested they should read a book in order, front to back. To them, that was like wasting time with filler material. The thought that this “filler” might be a source of surprise, inspiration and additional curiosity never crossed their mind before.

I get the comfort of quick answers from stack overflow, youtube videos or a chatbot AI. I see the instant gratification nature of going on a highlight-driven journey through nearly all topics of modern programming. But we aren’t creatures that thrive and prosper on instant gratification. We don’t learn from quick success. We learn by trial and repetition. And we can’t cheat our biological heritage (at least not yet).

So, what is my point? I think that “broad knowledge”, the ability to combine different aspects in thought experiments and slow, creative learning will be more important in the future, especially with the availability of a talking encyclopedia right in front of us that can fill the minor gaps faster than we can articulate the question. But we need to know what to ask, and even more important – why we ask.

An Indicator That You’re Leaving the Realm of Unit Tests

Automated unit tests are the grassroot foundation of a healthy test suite. But they aren’t the only type of automated tests that we need to write in order to test a system thoroughly enough to be confident about its production readiness.

There are things like end-to-end or even GUI based tests that have completely different testing mechanics that unit tests. It is clear just from looking at the test code that they aren’t unit tests.

But for the wide range of integration tests, there is a subtle and nearly impercetible transition from unit test to integration test that is hard to explain. It doesn’t really matter on which side of the diving line between the two test types you are as long as you are close to it. But as tests evolve, you need to apply different advancement strategies to the different types of tests. One goal is to keep unit tests from becoming integration tests over time, which is prevalent when factoring out system parts that were small at first.

When things are hard to explain, we search for indicators that can serve as objective counselors and help with making the decision. For the distinction between unit and integration tests, one such indicator is the distance between motor point and reaction point. Let me explain the concepts:

Let’s pretend we need to test the implementation of a baker (or a baking machine):

@Test
void can_produce_bread() {
Baker target = new Baker();
Bread actual = target.bake();
Bread expected = new Bread();
assertEquals(
expected,
actual
);
}

This is a straight-forward unit test in the AAA (arrange, act, assert) structure:

  • Arrange: We build the “test world” or the slice of the system that should be tested. We call it the “target” (some call it the “cut”, from “code under test”, which corresponds nicely with the “slice of the system”).
  • The target contains the motor point, the specific entry point where the code under test is “irritated” by calling a method. It is this irritation that causes the code under test to exhibit a certain behaviour that produces an observable result. The point where this result can be observed is the reaction point.
  • Act: We enable the motor point by calling the bake() method on our target baker. The code under test works its magic and gives us the result, which we call “actual”. The return value of the bake() method is the reaction point. It has two roles in the context of our test:
    1. It provides the observable result of the code under test.
    2. It serves as the last step of the code under test. The test framework leaves the code under test by returning the result. The exit point and the reaction point of the code under test are at the same spot (the distance between them is zero).
  • Assert: We compare the actual result of the code under test with our expected result. In our case, that’s a bit silly because we just want to have a bread, without any further attributes to it. But this blog post is not about the art of assertion, so we keep it simple and silly.

Let’s review the positions of the three named points:

If you read from top to bottom and left to right, the reaction point seems to be placed before the motor point. If you read it like a programmer should, you see that the point are positioned in their execution order: motor point, exit point, reaction point.

You also see that the distance between the points is very small and in the case of exit and reaction point only distinguishable if you look very closely.

That’s the indicator for writing an unit test: If your entrance to the code under test (the motor point) is effectively the same position as your exit from the code under test and the place where you get your actual result (the reaction point), you are unequivocally writing an unit test.

If the distances between the three points get larger, you are drifting away from unit tests and entering the big realm of integration tests. That is not necessarily a bad thing, sometimes it’s a necessity, but it should be a deliberate decision on your part and not an unnoticed accident.

Let’s look at an example where the distances between the points are larger:

@Test
void can_sell_prepared_goods() {
Baker given = new Baker();
Bakery target = new Bakery(
given
);
target.prepareGoods(1);
assertEquals(
Optional.of(new Bread()),
target.sell()
);
assertEquals(
Optional.empty(),
target.sell()
);
}

In this case, our baker now owns his own bakery where he can sell his breads to make a living. But baking breads “just in time” a customer requests one is not a sustainable business model, so the bakery has to prepare in advance and sell from the supply.

To test that we can fill up the supply and it gets emptied correctly, this test (in combination with other tests not shown here) does the AAA structure again:

We arrange our test world by inventing a baker and giving him to the bakery, which is the target in our case. We want to test the functionality of the bakery and a baker is required to do so. We already asserted that the baker knows his trade.

Then we act on our target. This is the motor point moment: We call the code under test to elicit a behaviour. But as you can see, we don’t receive a result right away. The effect seems to happen internally and we need to observe it from a different angle. Our reaction point has moved away from the motor point. And we have several exit points on our test journey. This is getting complicated!

In order to assert that the bakery’s supply holds one bread when told to prepare only one, we just buy two breads consecutively and see what happens. If there is only one bread in supply, we should get a bread the first time and nothing for our second purchase. The reaction point is now the sell() method, a good distance away from the prepareGoods() method we used as the motor point. Both points are (hopefully) connected by internal machinery in the bakery. We don’t want to assert the internal machinery, we want to assert its outcome. This requires the distance between motor point (“pressing a button up here”) and reaction point (“getting a product down here”).

You might argue that this example is still an unit test and I would agree. But we already see mechanics that occur predominantly in integration tests:

  • Elaborate arrange steps
  • act step without a return value (“actual” is missing)
  • Multiple assertions, telling a story with their order

When you imagine that the breads need to be of different kinds (dark bread, wholemeal bread, the whole german bread culture), you can probably see how the small unit test we just wrote kind of explodes with secondary complexity.

A realiable indicator that an automated test is going to be complicated is the distance between motor point and reaction point. Once you know about the concept, you can incorporate it into your testing intuition.

I hope it helps you write better tests or write good tests more deliberately. If you have thoughts about the concept, share them in a comment!

Implicit Protocol Requirements Can Drive You Mad

Some years ago, I had a software project that wanted to integrate a new kind of machinery into an existing application. Thanks to a modular and layered architecture, you could swap out the old machinery module and replace it with a new one. So it came down to writing an elaborate adapter between the existing application code and the new machinery interface. Shouldn’t be too hard, right?

And at first, it wasn’t. The machinery interface was relatively narrow, with just a few data registers to read from and write into. One core functionality of the old and the new machinery was moving equipment around at different axes (horizontally, vertically, etc.). The difference was: The old machinery was based on position switches, the new one operated on a sensor-based positioning system.

Position switches are rudimentary technology: An engine drives along the axis until it triggers the position switch that shuts of the engine. The advantage is a basic set of commands: Drive left (until you hit a switch) or drive right (until you hit a switch). This machinery control can be implemented by analog relais logic. The downside is that there is only guessing where the engine actually is at any moment if it doesn’t reveal its position by triggering a switch.

The new machinery works with a fancier method of positioning and movement. The control unit of the machine keeps track of the coordinates for every axis of movement. If you want the machine to assume a different position, you transmit the target coordinates and the machine moves until the difference is zero.

In reality, it wasn’t that easy. You also needed to transmit the desired velocity of the movement. The target was reached once the coordinates were equal to the transmitted coordinates and the actual velocity of all axes was zero again.

Okay, so making the new machinery move was a two-step transmission: First, you give it the target coordinates, then the speed values. And then you wait until things are like you want them to be.

The new module worked flawlessly with the new machinery. We could move it around in the boring one-dimensional ways the actual use case required or we could make it dance in complicated courses. The customer was pleased and the machinery was installed to perform the one-dimensional movements from now on.

The project was finished successfully. But after a while, the customer had a complaint. Seldom, but reocurring, the machinery would not move when commanded to, but blow a fuse and go into an error state.

Initially, the customer treated it as an electrical problem within the machinery. Until the manufacturer couldn’t find a cause and suspected my software to transmit faulty command parameters. I implemented an exhaustive logging of all transmissions and could prove that the parameters were as correct as they were boring. The application transmitted “full left” or “full right” for the horizontal movement and nothing else.

We were all stumped and out of ideas until I had an idea out of the blue:

What if the command interface to the machinery has a hidden assumption that is not met by the application?

But why did it work 99 percent of the time? Wouldn’t the assumption be present for every movement command?

Every time I hear “spurious failure”, I think about a concurrency problem. But my module worked strictly serial, one command after the other. There was nothing going on concurrently on my side.

And then it dawned me: The concurrent process is the main loop of the machine control unit. The machine control unit essentially runs a single thread that performs a series of steps in an endless loop: Check machine status, check command registers, apply commands, do other machinery stuff, repeat.

What if the “check command registers” step occurs right when my software is in the middle of transmitting the target parameters? It would read a partially written set of parameters. More specifically it would read new target coordinates, but not the necessary velocities. It would calculate delta distances and try to move, but with absurdly low or high velocities, depending on the formulas. If at any point a division by velocity occurs, it would divide by zero.

Because I couldn’t review the code of the machine control unit and the original programmer of it wasn’t available anymore, I tested my hypothesis by reversing the parameter write order: velocity first, location last.

And I wasn’t wrong: This little change got rid of the spurious failures.

The hidden assumption of the control unit code was that all parameters were transactionally valid at any given time. This translated to an implicit protocol requirement: All clients of the command interface needed to either

  • Transmit all changes at once (not possible with the technology that was used for transmission)
  • Transmit the changes in an order that has no effect until all changes are written.

The second option was what I implemented. Instead of “steer, then accelerate”, I needed to “accelerate, then steer”, because velocity without a delta distance would not move the equipment, but delta distance without velocity would attempt to do so.

One small sentence about the required write sequence in the documentation would still make this a “surprise requirement”, but a documented one. Without any documentation, its pure luck if a client pushes the buttons in the right order or not.

If you want one learning from this story: If a failure happens only occassionally, think about concurrency problems and include all periphery (humans, too!) into your scenarios.

“Keep in mind” code

Reading source code, especially those from other people (and that includes your past self of some months, too), is hard and needs practice. Source code is one of the rare forms of literature that is easier to write than to read. Yet it seems that code is only written once, but read multiple times during its lifetime. Every time we need to make a change to it, we need to read the whole block of code thoroughly and sift through the rest in order to find the relevant block.

This means that source code should be written with readability and understandability in mind. And in fact, I’ve never met a programmer that set out to write obscure code. We all want our code to be easy to read. And while I don’t have a silver bullet answer how this feature can be achieved, I’ve seen some patterns that are detrimental to the goal. I call them “keep in mind” code lines, because that is what you need to do:

You need to make a mental or physical note of some additional requirement that the source code imposes onto the reader, often without a discernable reason.

Let me make an obvious example:

while (true) {
    // some more code
}

This simple line of code requires the reader to make a note: There needs to be some construct that exits the forever loop in the “some more code” section or else the program wouldn’t work right anyway. We have two possibilities: We can interrupt our flow of reading and understanding, scan ahead looking for the exit structure and discard the mental note once we’ve found it. Or we can persist the note, continue with our reading and cross the note off once we read past the exit structure. Both reactions require additional effort from the reader.

If we choose the first possibility, we need to pause our mental model of the code that we’ve read and understood so far. This is equivalent to peeking some pages ahead in a riveting book, just to make sure the character doesn’t die in the current situation. It’s good for peak suspense, but we really don’t want that in our source code. Source code should be a rather boring type of literature. The stories we tell should fascinate on a higher level than “will this thread survive the method call”?

Recalling a paused mental model is always accompanied by some loss. We don’t notice it right away, but some aspect that we already knew goes missing and needs to be learnt again if relevant. In my opinion, that is a bad trade: My high-level model gets compromised because I need to follow a low-level distraction from one line of code.

If we choose the second possibility and make a written note on a piece of paper (or something equivalent), we might hold the mental model in place during the short interruption of writing the note. But we need to implement a recurring note checking mechanism into our reading process, because we shouldn’t forget about the notes.

There is a third possibility: Ignoring the danger. That would mean reading the code like letting the TV run in the background. You don’t really pay attention and the story just flows by. I don’t think that’s a worthwile way to engage with source code.

Let me try to define what a line of “keep in mind” code is: It is source code that cannot be understood without a forward reference further “down” the lines, but raises concerns or questions. It represents an open item on my “sorrow list”.

Another, less obvious example would be:

private final InputStream input;

Because InputStream is a resource (a Closeable) in java, it needs to be used in accordance to its lifecycle. Storing it into a member variable means that the enclosing object “inherits” the lifecycle management. If the enclosing object exposes the resource to the outside, it gets messy. All these unfortunate scenarios appear on my checklist as soon as I read the line above.

What can we do to avoid “keep in mind” lines? We can try to structure our source code not for writing, but for reading. The dangling mental reference of “I need to exit that while-true loop” is present even as the code is written. Once we notice that we keep a mental short-term list of open code structure tasks while programming, we can optimize it. Every code structure that doesn’t lead to some mandatory complementary work further down is one less thing to keep in mind while reading.

How would the example above produce less mental load while reading? Two options come to mind:

do {
    // some more code
} while (true);

This is essentially the same code, but the reader has seen all lines that exit the loop before being made aware that otherwise, it loops forever. The solution to the problem is already present when the problem presents itself.

Another option makes the exit structure explicit from the start:

boolean exitWhileLoop = false;
while (!exitWhileLoop) {
    // some more code
}

The exit structures in the “some more code” section should now use the flag “exitWhileLoop” if possible instead of breaking out directly. If necessary, a hearty “continue” statement at the right place omits the rest of the loop code. This option will lead to more code that is more verbose about the control flow. For the reader, that’s a good thing because the intent isn’t hidden between the lines anymore. If you as the code author think that your code gets clunky because of it, contemplate if the control flow structure is a good fit for the story you want to tell. Maybe you can simplify it, or you need to employ an even more complex structure because your story requires it.

In any case, try to avoid “keep in mind” lines. They burden your readers and make working with the code less pleasant. I have several more examples of such lines or structures, but wanted to keep this blog post short. Are you interested in more specific examples? Can you provide some example from your experience? Write a comment!

P.S.: I love the gibberish on the AI-generated checklist in the blog entry picture and wanted you to savor it, too.

Single-Use Webapps

One of our customers has the requirement to enter data into a large database while being out in the field, potentially without any internet connection. This is a diminishing problem with the availability of satellite-based internet access, but it can be solved in different ways, not just the obvious “make internet happen” way.

One way to solve the problem is to analyze the customer’s requirements and his degrees of freedom – the things he has some leeway over. The crucial functionality is the safe and correct digital entry of the data. It would suffice to use a pen and a paper or an excel sheet if the mere typing of data was the main point. But the data needs to be linked to existing business entities and has some business rules that need to be obeyed. Neither paper nor excel would warn the user if a business rule is violated by the new data. The warning or error would be delayed until the data needs to be copied over into the real system and then it would be too late to correct it. Any correction attempt needs to happen on site, on time.

One leeway part is the delay between the data recording and the transfer into the real system. Copying the data over might happen several days later, but because the data is exclusive to the geographical spot, there are no edit collisions to be feared. So it’s not a race for the central database, it’s more of an “eventual consistency” situation.

If you take those two dimensions into account, you might invent “single-use webapps”. These are self-contained HTML files that present a data entry page that is dynamic enough to provide interconnected selection lists and real-time data checks. It feels like they gathered their lists and checks from the real system, and that is exactly what they did. They just did it when the HTML file was generated and not when it is used locally in the browser. The entry page is prepared with current data from the central database, written to the file and then forgotten by the system. It has no live connection and no ability to update its lists. It only exists for one specific data recording at one specific geographical place. It even has a “best before” date baked into the code so that it gives a warning if the preparation date and the usage date are too distant.

Like any good data entry form, the single-use webapp presents a “save data” button to the user. In a live situation, this button would transfer the data to the central database system, checking its integrity and correctness on the way. In our case, the checks on the data are done (using the information level at page creation time) and then, a transfer file is written to the local disk. The transfer file is essentially just the payload of the request that would happen in the live situation. It gets stored to be transferred later, when the connection to the central system is available again.

And what happens to the generated HTML files? The user just deletes them after usage. They only serve one purpose: To create one transfer file for one specific data entry task, giving the user the comfort and safety of the real system while entering the data.

What would your solution of the problem look like?

Disclaimer: While the idea was demonstrated as a proof of concept, it was not put into practice by the customer yet. The appeal of “internet access anywhere on the planet” is undeniably bigger and has won the competition of solutions for now. We would have chosen alike. The single-use webapp provides comfort and ease-of-use, but ubiquitous connectivity to the central system tops all other solutions and doesn’t need an extra manual or unusual handling.

How to Eat Last

A good book about leader mentality is “Leaders Eat Last” from Simon Sinek. The book is not about your diet, but your approach towards your subordinates and your peer group.

I don’t want to recapitulate the content of the book – it is worth the read or at least a presentation about it. I want to talk about one specific implementation of the principle in my company that I did even before reading the book, but could only name and highlight after Simon Sinek lend me his analogy.

I’m a software developer and founded a software development company. I hired other software developers and they develop software with me. I might be the founder, owner and director of the company (so, in a short team, the “leader”), but I’m still a fellow developer and understand the developer’s mindset. So I know what a developer wants, because I want it, too.

Except, I make sure that I’m the last one in the company to get it.

Two examples:

We bought our second round of office desks in 2010, when we moved into a new office. They were still traditional desks that could only be height-adjusted with tremenduous effort. We only did it once and settled for “good enough”. Our first electrically height adjustable desk was bought in 2013 because of a specific medical requirement. But it opened the door to the possibility of having the desk at any height throughout the day. You might even work standing up.

We slowly accumulated more electrically height adjustable desks until we had 50 percent classic and 50 percent electric desks. At that point, I bought the other half at once (and they are fancy “gamer nerd” desks, because why not?). The last classic desk in the company was my own. I replaced it with the oldest electric desk in the portfolio. Now I can work while standing up, too.

When the Corona pandemic hit in 2020, we moved to home offices all of a sudden. I wrote about this change several times on this blog. This physical separation led to an increased demand for video calls. I made sure everyone is covered with the basic equipment (webcam, headphones, etc.), including me. But I also experimented with the concept of a “virtual office”. It consisted of a video meeting room that I hung out in all workday. I turned the camera and microphone off, but was instantly present if somebody had a desire to talk to me – just like in the real office. For this use case, I installed an additional monitor on my setup, the fourth one, called the “pandemic display” in a blog post about it. Because I didn’t know if the experiment would work, I bought the smallest and cheapest display available for me.

The experiment went fine and I decided to equip everyone with an additional “videoconference display”. The new models were bigger and better. If an employee didn’t see the benefit of it, I didn’t force them to install one in their home office, but every workplace in the office has at least four monitors. Guess were the original one is still installed? I made sure everybody had a better monitor than me.

With this process, I can guarantee that my employees have the work equipment that is good enough for their boss. Because I have it too – or something inferior. If I feel the need to upgrade my gear, I upgrade everybody else and then lift my things to their level. If I feel comfortable with my gear, so does everybody else (except for individual demands and we have a process installed for that, too).

I love self-regulating systems and this is one: The whole company is equipped in a manner that is sufficient or even better for me to do the work. If I want more or better things, everybody gets the upgrade before me because only then do I allow myself to have the same luxury. No “upward” exception for the boss, and only temporarily “downwards”. My wants and needs define the lower limit of equipment quality for all of us. If I can’t buy it for everyone, I don’t buy it.

That is the whole trick: Equip yourself last or lowest. You can be sure everybody is well-equipped that way. Thanks, Simon!