SOLID helped me long before I started using AI in development. It helped me split a complex brief into parts I understand, develop them and not have to deal with the whole application again with every change.
Today it makes even more sense to me.
When working with AI, I still need to know what is being created in the system, why it belongs in that particular place and what I can expect from it. I need to find my way around it later too. And also at the moment something stops working.
So when I come back to SOLID, the first thing that comes to mind is not five definitions. It is concrete situations in which these principles helped me. Over time, I have built my own way of working around them.
One such situation has stayed in my memory to this day.
When I do not remember the code, but understand its language
Roughly two to three hundred people worked on one project. In my part, I had the opportunity to consistently hold to rules for dividing responsibilities and for naming.
About three years after a particular part was handed over, a colleague came looking for a class. I think it was related to subscriptions.
I did not remember where I had written it. In fact, I no longer even remembered exactly how we had put that part together. I asked him to describe what it dealt with.
From his description, a name along the lines of PaymentCreditBalanceRequest came out.
We typed it into the search. And under exactly that name we found the class we were looking for.
What struck me was that I did not need my own memory of the implementation to find it. It was enough to go through the way of thinking by which it had originally been created.
This is exactly what a predictable system means to me. I do not have to know every place in it by heart. From what I already understand, I can work out where to look next.
What a name tells me
With Single Responsibility, I care whether I can clearly define the responsibility of a given part. The point is not to create the smallest possible classes, but to separate things that change for different reasons. [1]
In everyday work, I help myself with three rules:
- What is in a function's name must be in its behaviour.
- What a function does must be recognisable from its name.
- A function's name should fit into twenty-five characters.
I am talking about the meaning of a function, not about the need to list every technical step in its name. And the limit applies to functions, not to all names of classes and messages.
Imagine a function that draws a square. A new requirement comes in and I add a condition to it: sometimes it should draw a triangle as well.
The code can still be short. But when I try to honestly name what it does now, I find that the original name is no longer enough.
For me, that is a useful moment. I am not looking for a shorter abbreviation to fit under the limit again. I want to know whether the purpose of the function has changed, or whether additional work has crept into it that I need to separate.
An if on its own is not a violation of the principle to me. What interests me is the change in meaning it brought.
Twenty-five characters is not a universal measure of quality. It is a guardrail that makes me stop in time. It often shows me growing responsibility before I start perceiving it as a technical problem.
I need to recognise one meaning in another place too
Names matter to me for one more reason. I need us to use a shared language.
A request ends in Request, an event in Event. For the same thing we use the same term. When moving between two parts of the application, I do not want to have to find out whether a different name means a different thing, or just a different author.
I give great weight to consistency here. I consider it more important than an individual's effort to find a slightly more precise word with every change.
When a shared term is misleading, I need to fix it together. When everyone fixes it their own way, we stop understanding each other.
The story of the class we tracked down was not, for me, the result of one well-chosen name. It was the result of us holding to the same way of naming over the long term.
And this is where code design meets the work of people. A rule on its own holds nothing together. I need discipline, code review and a willingness to come back to the shared agreement.
What already works should keep making the same sense
With public APIs, I started to perceive this agreement even more strongly.
We ran several versions side by side and promised customers we would support them. But I could not see into their implementations. I could not verify that a change was correct based only on how our own client behaved.
When I add an optional input parameter and keep the existing behaviour when it is omitted, it can be a backward-compatible extension. But when I turn an optional field into a required one, some customer may not be sending it at all. The same applies to tightening validation so that it starts rejecting requests that were valid until now. [2]
Sometimes very little changes in the code. From the customer's point of view, what they relied on stops being true.
We set the rules for these changes in advance, partly following a security analysis. Thanks to that, we did not have to look for the line between an extension and a new version again with every release.
With Open/Closed, this experience meets me in the question of what should stay stable and where room for new behaviour should be created. The principle itself points towards extending through well-chosen boundaries, without having to change the stable parts every time. [3]
API versioning is not its definition. It is one of the situations in which I need to think rigorously about what I am extending and what I am already replacing.
Replace the implementation, keep the agreement
For a long time I used Liskov Substitution rather intuitively. What came to mind was the need to test well or to replace the connection to an external service, not the name of the principle itself.
One time I need a production implementation, another time a test implementation. Later perhaps a different provider. I want this swap not to require a reaction from everyone who uses the service.
But the same methods do not yet mean the same agreement. An implementation must preserve what a user of the shared interface can legitimately rely on. This substitutability of behaviour is exactly what Liskov Substitution is about. [4]
Imagine that a successful result means confirmation that data has been stored. A new implementation, however, uses it to confirm only that a request has been accepted, to be stored later. The name and the return type can stay the same; the meaning of the result has changed.
Small, clearly defined parts help me see this boundary. On their own, they do not verify it. I need to know what the contract actually promises, and judge my test implementations by that as well.
This matters to me outside code too: the ability to replace how something is done is not the same as the ability to quietly change the expected result.
I can assemble the whole differently from the first time
With Interface Segregation, the number of methods is not what matters to me. I care about what a particular user of the interface needs and whether I am forcing them to depend on something else as well. [4]
A time provider will naturally be small. I need to get the time and have it under control in tests.
With a delivery system, the situation is different. I prepare messages, sort them, sometimes group them for one user and finally send them. On the outside, this process can be covered by a single, fairly extensive facade.
Composing it from smaller interfaces and components has worked well for me. A part that only needs to send then works with just the corresponding interface. It does not need to know the whole delivery process because of that.
When building a pipeline for processing and sending messages, this kind of division let me rearrange the workflow according to the customer's requirements. The individual steps had their place and meaning, but their original arrangement was not something I had to keep forever.
That is exactly what interests me about composition. Small parts do not have to lead to a small result. I can assemble a larger whole from them and later assemble it differently.
Code should serve the business, not be a sculpture nobody may cut into.
The core of the application does not have to know its whole surroundings
Dependency Inversion helps me create a similar boundary between the business logic and the technical environment. The core depends on abstractions expressing its needs, not directly on specific infrastructure implementations. [5]
On the project for the state treasury, we did not know the final production environment at the start. But we needed to develop and test.
Thanks to separate components and interfaces, we could create our own environment, simulate the required behaviour of the surrounding services and work on the business logic. As the form of the infrastructure became clearer, we could replace the relevant connections.
What interested me was exactly this difference: I change the way the application communicates with its surroundings, but I do not have to reopen the business rules that have not changed.
I wire up the specific implementations when assembling the application using Dependency Injection. That is a wiring technique; Dependency Inversion is the principle of the direction of dependencies. [6]
For me, the greatest value is not in the mere ability to replace an object. It is in the work I do not have to do again when replacing it.
What has not changed for me with AI
When I look at these experiences today, I see no reason to move the boundary of understandability just because AI helps me with the implementation.
An error can still come. I can still end up in a situation where I have no access to a debugger or to complete logs and piece the cause together from several indicators. Clearly separated responsibilities have helped me narrow down the search in such situations. They do not replace logging.
I think about the brief for an AI agent in a similar way. I want to be able to say which part should change, what should stay intact and how I will verify the result. If I cannot express that, I need to go back to my own understanding of the brief.
In consistent names and small contracts I also see a way to make available to an agent the intent that would otherwise remain only in people's heads. I do not take it as an automatic guarantee of the right result. It is the reason why I care about what can be read from the system itself.
Perhaps this is what matters most to me about SOLID today. It helps me not only to divide the work, but also to recognise whether I still understand it.
From rules to your own experience
When I go through these rules with a team, I usually do not need to look for another story from my past. We take the code people are working on right now.
We try to name a function's responsibility. We look at what has changed in its meaning. We go through what the caller relies on and what would happen if the implementation were replaced.
This is where real learning begins for me. Agreeing with a definition is not enough. I need to recognise it in a situation in which I am the one deciding.
I do not have to rebuild the whole application for that. I can start with one place where we repeatedly get lost during changes, and watch whether the new division really helped us.
When I go back to the colleague with whom we looked for the class after three years, this is exactly the result I care about. Being able to understand again what was created, even when I no longer remember the circumstances in which it came about.
SOLID helped me with this before AI. Working with it, I do not want to lose that ability.
Sources and further reading
The project stories and naming rules come from my own experience. The following texts expand on the technical principles I connect them with.
Sources verified on 14 October 2026.
[1] Robert C. Martin. The Single Responsibility Principle. Responsibilities and reasons to change.
[2] Google. AIP-180: Backwards compatibility. Preserving the behaviour of public APIs when extending them.
[3] Robert C. Martin. The Open Closed Principle. Stable parts of a system and the boundaries of extension.
[4] Robert C. Martin. Solid Relevance. In particular Liskov Substitution and Interface Segregation.
[5] Microsoft. Architectural principles. The direction of dependencies and separating application logic from infrastructure.
[6] Martin Fowler. Inversion of Control Containers and the Dependency Injection pattern. Wiring up implementations and separating configuration from the use of services.

