Transcript: Programming as Theory Building and the Not Invented Here Syndrome (english) December 28, 2025
This is the translated transcript of my talk “Programming as Theory Building and the Not Invented Here Syndrome” at the InfoDays: Softwarearchitektur 2025. It is a rather direct translation, so expect some rough edges.
(Das deutsche Originaltranskript ist hier.)
Intro
Hello, welcome to my talk “Programming as Theory Building and the Not Invented Here Syndrome”! My name is Sebastian Hans. I have been working in software development for 20 years, most of the time as a software architect and developer in development teams; and this is the setting for the ideas you are about to hear from me.
Currently, I am a Senior Technical Consultant at bbv and accompany software and digitalization projects for our customers. When you do this for a while, sometimes you see things go well, and sometimes they get a little bumpy, occasionally due to technology, but often due to people, too. And that is what we will deal with in this talk, “Programming as Theory Building and the Not Invented Here Syndrome”. In it, I will first briefly introduce the essay “Programming as Theory Building”, then the Not Invented Here Syndrome, and then I will examine the connections and show situations in which they influence the daily life of development teams.
Programming as Theory Building
Programming as Theory Building is an essay by Peter Naur. It is 40 years old now, but its topic is still just as relevant as it was in the 80s. I recommend to everyone to read it themselves some time; there is a lot of wisdom in it. For our purposes here, it is enough to know the core thesis: a program does not consist of its source code, but of a theory. And programming does not consist of translating facts from the real world into source code, but of building this theory. The theory – and thus the program – lives in the heads of the developers.
(A small side note: In the following, I am going to talk about “programs” because Naur's essay uses this term. But everything applies to systems and system compositions as well – just to make that clear.)
Therefore, to really understand a program in this sense, reading the source code is not enough; and neither is looking at the documentation; you somehow have to get at this theory.
The term “theory” that is used here does not only refer to factual knowledge, e. g., what the code does, where to find which functionality and so on, but also the ability to explain it, to justify it, and to connect it to the real world. You can imagine the code to be like the shadow of a real object. Here we see a shadow. If I only look at this, I can see it is a circle. But this on its own does not constitute a theory, because I have no idea what creates the shadow. And there can be quite different theories. It could be a sphere, for example. Or a cylinder. Or a cone or an Easter egg. If I am in possession of the theory, I can explain why the shadow is circular. Let's take the cylinder as the theory. Then the reason is because it is being illuminated along its axis. And if someone comes along and says, “Look, our requirements have changed by 90°, make sure the shadow matches!”, then I only ask for the axis, and if it is the vertical axis, then I say: “OK”, and – boom – I am done. Someone who is not in possession of the theory and only sees the circle cannot know what needs to be done – and they also cannot somehow reconstruct this knowledge from the circle. That simply is not possible.
Whether this theory is present or not is especially noticable when changes or extensions are needed. There are usually many ways to somehow shoehorn in a requirement, but often only a few that fit the theory. If you choose one of those, the change is relatively easy and the result remains easy to change and maintain. If you do it differently, you create an unmaintainable pile of code that might even work if you are lucky.
An example
An example from my own practice: For a few years, I was in a team that was responsible for a payment component. This component provided a unified interface through which apps could process payments from customers. It had different payment service providers connected on the back end for different payment methods. The payment component has a ports-and-adapters architecture, which means, in short, it separates domain logic from technical connections to surrounding systems, which live inside adapters.
At one time, a new colleague joined the team and was given the task of implementing a new, additional payment flow. To do this, he made extensions in the domain and in the adapter for the affected payment service provider. Now I have to say that, for various reasons, the boundary between generic domain logic and specifics for the payment service provider, which belong in the adapter, is not that sharp. Of course, the ports-and-adapters architecture itself is documented, but that does not help you decide exactly which piece of the implementation to put in which box. This is part of the theory that lives in the heads of the developers and that you cannot directly find anywhere else. Long story short, the new colleague did not fit the feature in the way it was intended. He pulled too many adapter specifics into the domain code, making this part too complex, and we then rearranged it together quite a bit to simplify subsequent changes. Doing this, we achieved two things. On the one hand, we brought the changes into alignment with the theory of the payment component and thus quite practically improved the maintainability of the software. And on the other hand, the colleague brought another piece of the theory into his head and can thus better drive the development forward within the bounds of the theory.
So again: The core thesis of Programming as Theory Building is that a program is more than the source code plus documentation. Rather, the program consists of the underlying theory and lives in the heads of the developers. Source code and documentation show, at best, a small part of it.
So much for “Programming as Theory Building”. Let's move on to the other concept, the Not Invented Here Syndrome.
The Not Invented Here Syndrome
Not Invented Here refers to the tendency to reject things that one has not discovered or invented oneself. In the context of software development, these can be tools, processes, programming paradigms, or indeed also a theory that underlies a program. This is not a formally defined term but rather an observation from practice, but there are studies that show that the effect is real. For example, this study from 2014. In it, researchers investigated how organizations conduct crowdsourcing, that is, ask the “crowd” – external people, customers, website visitors, etc. – for improvement suggestions; and in particular, how they then evaluate and implement them. To do so, they measured various types of distance and found, for example, that suggestions that were closer to what the organizations were already doing anyway received more attention than genuinely new suggestions that came from somewhere in the crowd – which were Not Invented Here. What is Not Invented Here is not that interesting to organizations.
I have experienced this myself as well. One of the clients of the payment component (the one I already mentioned) was an SAP system. But it was not connected the same way as all the other clients. When this connection was being planned, there was a dispute between those responsible for the payment component and those responsible for the SAP system because the SAP people said that the flow as the payment component team envisioned it (and as the other clients had implemented it) would not work with SAP; it would contradict the SAP logic. Not Invented Here. We can't do that and we don't want to.
Because this Not Invented Here reaction is usually adverserial, it has a somewhat bad reputation. It is also called “Not Invented Here Syndrome”, and a syndrome is fundamentally something bad. But the Not Invented Here reaction, as I would like to call it more neutrally, does not necessarily have to be bad. A small dose of Not Invented Here is good and even necessary, especially if you take Programming as Theory Building seriously.
Not Invented Here as a sensor
To understand this, I would like to return to the idea of distance used by the study. Very close is almost Invented Here, far away is definitely Not Invented Here. For the purpose of this talk, I will take a closer look at the distance of an idea or concept to the theory of the program. I'll call it “conceptual distance”. The distance is 0 if an idea lies within the bounds of the theory. And if a concept does not fit in with the theory at all, then I say the two have a high conceptual distance. From the perspective of the theory of the program, such an idea would be Not Invented Here.
If you then think of a program – in the sense of theory + code – and an idea for a change, then it would be good if the idea had as little conceptual distance as possible to the theory. Because then it would usually be easy to implement, relatively low-risk, and would leave the program in a good, maintainable state. Whereas a change that was far away from the theory would be rather difficult to implement and error-prone and could potentially destroy the conceptual integrity of the program and thus render it unmaintainable. Hence, it is completely rational to view ideas with a high conceptual distance to the theory of the program more critically than those that are closer to it – which is exactly what “Not Invented Here” is about. From the perspective of the development team, Not Invented Here is basically a protective shield for the conceptual integrity of our theory. Or a sensor that tells me, “Watch out, this is something that does not fit our theory.”
Does that mean Not Invented Here is always good and we should make a habit of rejecting ideas that are Not Invented Here? Of course not. Blanket statements are always wrong. For one thing, there are many different types of distance where one could cry “Not Invented Here”; distance based on group membership, for example. Something like that is usually bad. That is not what I mean here. I exclusively mean conceptual distance.
And there is another reason why I should not categorically reject things that are Not Invented Here: the theory of the program may not be perfect. From time to time, the team learns something new and adapts the theory. This is only possible by integrating ideas that lie outside the bounds of the theory.
Dealing with new ideas
So what do I do if I am a developer in an established development team and someone comes along with an idea or does something in a way that triggers my Not Invented Here sensor? First, I quickly check whether it has triggered for a good reason. Is it the conceptual distance from the theory? If it is not, then maybe it is a false alarm and I should mute it and just continue listening to the idea. But if I find that what I am hearing or seeing really is far away from the theory of my program, then I can rejoice because then the sensor has worked. It has prevented us from blindly putting something into our program that will cause problems later.
The simplest solution is to say, “No, we won't do that.” This saves time and money, and keeps the program tidy and small. But of course you cannot and do not always want to do that, because behind many ideas there is a good intention. Then the task consists of reducing the distance to the theory as much as possible. Is there a way to represent what you actually wanted to achieve with the idea within the existing bounds of the theory? This is the best case; then you should just do it that way. Remember the new colleague in the payment component team. He wanted to build a feature and tried it in a way that did not fit the theory, and the team then remodeled it together to work well within the existing theory.
If we cannot bring the idea conceptually closer to the theory, but we still want to implement it, we can do that, but it will be more work. Because then we have to adapt the theory to the idea. And that means not just writing new code, because the theory is more than just code. We have to bring the whole team along and together work out a new, consistent mental model that reconciles the existing theory and the new idea; and then we can implement that. The main task here is to establish a shared understanding of the new theory. Writing the code to match afterwards is usually much easier.
Another example
I also have an example for that, from the same payment component. We once had to replace a payment service provider. The contract with the old one had expired, a new one had been found as part of a tender process and had to be integrated, without downtime. We already had a way to configure the payment service provider per app, to send the payments for each app to the appropriate adapter. That means, following the theory of the program, we could have switched the apps over via a simple configuration change at the time of migration, and we would have been done. But since many customers were affected, a lot of money was at stake, and big-bang migrations are quite risky, I had the idea of performing the migration per customer instead. This way, we would have much more fine-grained control, and we could significantly reduce the risk.
However, the conceptual distance to the existing theory was relatively large, and so – even though the idea came from me, that is, from within the team – we carefully considered whether to introduce it like that and how to marry this concept of migration per user with what was already there. Ultimately, we decided to completely remove the configuration per app, implement the migration per user, and remove it (along with the old payment service provider) after the migration was finished. The last part may surprise you, but it had to do with the new theory we had formed on the subject of routing payments to payment service providers.
Old theory:
Routing is not really proper logic. It is enough to have a routing table somewhere that can be changed arbitrarily at runtime.
New theory:
Routing is a core function of the payment component, and changing it is a non-trivial undertaking that will probably involve custom logic and edge cases that are best represented in code.
If we had not taken the time to adapt the theory, but simply bolted the migration per user on top … I don't even want to imagine what that would have come of that. Certainly nothing that would have been easy to maintain and extend.
Dealing with existing theories
Let's look at the whole thing from a different perspective, namely from the point of view of someone who wants to or has to deal with someone else's theory. This could be, for example, a piece of software that someone else has developed and where something needs to be changed now. And let's also assume that the conceptual distance to what I am otherwise used to is medium to high. Then I will initially have difficulties understanding the theory behind it. I won't be able to carry out the change to the software in such a way that I can be sure the result will be good. The theory is too far away. In other words, my Not Invented Here sensor is being triggered. But that does not mean I should fall into the Not Invented Here defensive posture. Because the consequence of the Theory Building thesis is: if I don't want a half-baked kludge, then I first have to obtain the theory of the program. I have to get it into my head. How to do that depends on the situation.
Onboarding to an active development team
One possibility is that I am joining an established development team. Then I can assume that the people already have a theory in their heads, but I don't know it yet. I only see the source code at first. Then the first thing to do is to get out of the defensive stance! The most unproductive thing to do would be to say things like, “That's strange! How could anyone come up with such an idea? I would have done this completely differently.” And so on. This would only prevent me from completing my main task, which is to internalize the theory of the program as quickly as possible. What helps is
- talking to the team,
- reading documentation,
- talking to the team,
- reading code,
- talking to the team,
- maybe starting with coding, using pair programming if possible, but at first less to adapt the code, and more with the goal of learning.
Oh yes, and talking to the team.
I've encountered such a situation recently with a service that handles historicized data. In this service, attributes of some entities can change over time and they are stored with “from” and “to” dates. Others are not. Some relationships between entities are historicized, too, and some aren't. I can see in the code that this is so, and the internal wiki contains some amount of documentation, but I only really understood the why behind it in conversation with the team. I could have said directly, “This is all way too complicated, I would have built it completely differently.” Not Invented Here. But that would not have helped me understand the topic.
So whenever I'm new in a team, I switch off the Not Invented Here defensive posture and try to absorb the theory as quickly as possible.
Dealing with legacy software
A different situation would be taking over responsibility for an existing system where nobody is available to answer questions. This can have different reasons. Maybe it is an old system where nothing has had to be done for a long time, and the people with knowledge of it have since moved on. And now there is something to do again and I'm the lucky guy who gets to do it. Or maybe a team exists, but it no longer consists of the original developers but of people who joined later and have not internalized the theory themselves. I know the experience – maybe you know it too – of joining a team that does not know its own system – or only parts of it. Then it gets difficult; then you have to reconstruct the theory. This is hard work.
And with this work, too, my Not Invented Here sensor or the idea of conceptual distance can help. Because it tells me what I still need to investigate. From the start, I already have a certain idea of what the theory could look like, even if it is only my intuition based on my experience with other programs. When I then look at the material that is there – code, diagrams, documentation, whatever – I see things where I think, “Yes, OK, fine.” And I see things where my distance sensor triggers – the one for conceptual distance. Then I think, “Wow, that does not fit at all with what I have pieced together so far!” Some developers then fall into the Not Invented Here defensive stance and grumble about the legacy crap they have to deal with. I take that as a signal to take a closer look, because this is a place where my theory is obviously still wrong or at least incomplete. So I dig in further and try to understand exactly that spot and reconstruct the original theory a bit further.
This works up to a certain point. But usually not completely. When you have reached that point, then you can – and should – begin to rebuild the theory to suit the active developers, to get out of this “I have no idea what I am doing” mode. The result is a theory that is actually alive in the heads of the developers, and you can continue working with it.
An example of this is a developer team that inherited a Java library consisting of five layers. Nobody knew the purpose of these layers, and some developers had exactly this Not Invented Here reaction where they didn't want to touch the thing at all. When I joined the team, we looked at what problems we actually had and where layers helped us and where they didn't; and we have now started to change the code according to our new theory.
Reprise
So you see, there is a connection between the idea of Programming as Theory Building by Peter Naur and Not Invented Here. To summarize briefly, Programming as Theory Building asserts that the essence of programming consists not of writing code, but of building a theory. This theory lives in the heads of the developers and is the actual program. The code is part of it, but it only ensures that the program becomes executable on a machine. Changes to the code that are in harmony with the theory are much easier to carry out and lead to better results than changes that are made without regard to the theory – because the program consists mainly of the theory.
Not Invented Here is a reaction to ideas that come from far away, where the distance can lie in different dimensions. This reaction is often dismissive and counterproductive, but it does not have to be that way.
One dimension that is particularly important for development is the conceptual distance. How far apart are two concepts or ideas or theories? According to the Theory Building thesis, program changes with a high conceptual distance to the theory are precisely the problematic ones. And this is why the Not Invented Here reaction can be useful, because it alerts me to this situation.
When I have learned to get rid of the automatic defensive stance that is often associated with it, and view it as a signal instead, then I am free to consider whether and how I want to reduce the conceptual distance. Maybe I don't want to do this at all and say, “No, I won't do that!” But if I want to do it, I have essentially three possibilities: I can adapt one side or the other, or I adapt both and land somewhere in the middle.
We have looked at a few situations: when onboarding a new developer, the two relevant concepts are the theory of the program and the preconceived ideas and notions in the developer's head. In this case, I usually want the developer to first understand the theory of the program and later bring in their own experience to improve the theory.
With change proposals, two things can happen. Either you manage to implement the requirements within the bounds of the theory of the program – that is the best case – or you actually have to adapt the theory of the program to integrate the solution idea. But then really change the theory, not just isolated pieces of code!
When taking over a legacy system where the theory is no longer accessible, you will often be unable to fully reconstruct it. But you should try, and then adapt the remaining parts to your own ideas and form a new theory so that you can work with it properly.
So, the next time you encounter a Not Invented Here reaction, in yourself or in others, think about what caused it and whether it might not be a signal to engage more deeply with the topic!
Thank you and happy programming! Or theory building!