Skip to main content
The Road to Architecture

I started with C++. Then came Delphi. After that, there were other tools, platforms, and technologies. Looking back, almost everything has changed over the years: programming languages, operating systems, approaches to interface development, tools, and even our understanding of what software should look like.

Yet some questions have remained the same.

I kept encountering situations where a solution that worked perfectly well under certain conditions began to cause problems under others. The differences between 16-bit, 32-bit, and 64-bit systems, the peculiarities of rendering and display, interface development, and platform limitations — all of this required more than simply knowing a programming language. I needed to understand how the system worked, why it behaved the way it did, and what consequences a particular implementation choice would have.

Back then, suitable tools or ready-made solutions were not always easy to find. Sometimes, I had to look for workarounds, explore third-party libraries, and learn to use products that offered capabilities missing from Microsoft's standard development tools.

Over time, I made my way from Borland products to JetBrains tools, Android development, and modern frontend technologies.

It wasn't simply a transition from one development environment to another. Every new platform brought its own limitations, possibilities, and questions that needed answers.

Gradually, I found myself thinking more and more not just about how to implement a particular feature, but also about which solution would make sense in the long run. How difficult would it be to maintain? What would happen when the requirements changed? How much time would further development take? Could today's convenient solution become a source of constant problems tomorrow?

And, of course, there was always the question of cost. Development time is limited, project resources are finite, and the client needs a result that makes sense not only technically but economically as well.

Money is not just the lifeblood of war. It is also the lifeblood of any complex technical process — and especially of the IT industry. It makes it possible to develop, maintain, and evolve software systems. And sometimes, economic circumstances rather than an architect's technical preferences determine which technology or architectural approach will ultimately be chosen.

It is easy to get carried away by a new, fashionable framework, enthusiastically start designing a system around it, and only later discover that the price of that decision is too high. Sometimes, it is far more useful to pause, carefully examine the available options, and consider what will happen to the project several years down the road.

How will it evolve? How much will it cost to maintain? How easy will it be to find people who can work with it? Could today's technological advantage become tomorrow's limitation?

For me, architecture is also about resisting the first impulse, however attractive it might seem. It means not reaching for the latest tool simply because it has appeared and promises to solve every problem, but first understanding what it will actually bring to the project — and at what cost.

Sometimes, the right choice is a new technology. Sometimes, it is a tool that has stood the test of time. And sometimes, the more sensible decision is to abandon the idea of building something ourselves and use an existing product instead.

What matters is that the choice should be driven neither by fashion nor by a developer's personal preferences, but by an understanding of the problem, the project's long-term prospects, and the resources available to us.

Over time, this perspective began to take shape not only through working with technologies, but also through working with existing projects. Starting development from scratch and gradually making decisions as new requirements emerge is one thing. Arriving at a project where much of the work has already been done, deadlines are approaching, and the team is struggling to meet requirements that somehow demand more and more time and effort is quite another.

In such situations, the consequences of decisions made long before your arrival become particularly visible. They are not always documented, explained in code comments, or even consciously recognized by anyone. Yet they are there — in the application's structure, in the way its parts interact, in duplicated code, and in the habits developers have formed when solving problems.

Sometimes, simply looking at how a project is organized is enough to understand why its development is progressing more slowly than expected. New functionality becomes so tightly coupled with existing code that every change requires checking numerous other parts of the system. The same code has to be fixed in several places. Developers spend their time not so much solving the actual problem as figuring out where something needs to change and what might stop working as a result.

And yet, the problem is not always a lack of experience on the part of individual programmers. Each of them may be perfectly capable of handling their immediate tasks, while the team's overall results still fall far short of expectations. Sometimes, the cause lies at another level: in how the project is organized, how clearly its boundaries are defined, and whether there are shared rules that allow different people to work on the same system effectively.

I gradually began to notice that these problems kept recurring regardless of the technologies being used. Programming languages, libraries, build tools, and approaches to interface development changed, but the consequences of certain decisions remained remarkably similar. Code became harder to modify, responsibilities between application components became less clear, and a seemingly simple task gradually turned into a chain of changes that had to be coordinated with one another.

This was where the difference between writing working code and building a system that could continue to evolve became increasingly apparent to me.

Working code solves today's problem. But a software product rarely remains unchanged after its first release. Requirements evolve, new use cases emerge, functionality expands, and new developers join the team. Sometimes, the platform or environment in which the application must run changes as well. That is when it becomes clear how well the original decisions accounted for future changes.

Of course, it is impossible to anticipate everything that might happen to a product over several years. Trying to do so would be pointless: requirements can change so significantly that some of the original assumptions no longer make sense. But there is a substantial difference between trying to predict the future in every detail and not thinking about it at all.

I became increasingly concerned with understanding which decisions we were making and how expensive they might be to change later. Where could we afford a simple solution that would be easy to replace if necessary? And where was it worth spending additional time on discussion and design because a mistake would affect too much of the system?

Over time, I came to believe that architecture is not about defining every class, component, or method in advance. Its purpose is much broader. It helps establish the system's fundamental boundaries, distribute responsibilities, define how its parts interact, and create conditions in which further development does not continually make the existing system more complicated.

And the more experience I gained, the less I wanted to think of architecture as merely a technical construction.

After all, even a well-designed system does not exist in isolation. It is built by real people with particular experience, knowledge, and habits. They work within deadlines, budgets, and technical constraints that are not always under their control. A solution that looks convincing on an architectural diagram may turn out to be entirely impractical if the team does not understand its principles or lacks the skills needed to implement and maintain it.

I had to learn to account for that, too.

I began to approach project preparation differently. It was no longer enough simply to gather requirements, select a suitable set of technologies, and determine which tasks needed to be implemented. I wanted to understand first who would be working on the system, what experience the future team members had, what constraints the existing environment imposed, and what the product was actually expected to become.

Discussing the MVP, future plans, the team's capabilities, and the client's requirements gradually became part of my architectural work. Not a separate organizational stage to complete before programming began, but a way of understanding the problem itself.

An architectural decision cannot be separated from the conditions under which it must be implemented. What works well for one team and one product may be a poor choice for another. Even a familiar technology that developers have used for years is not always the optimal option. Conversely, there is no need to introduce a new tool simply because it offers additional capabilities.

I believe it is important to understand what we want to achieve and what conditions we need to account for before choosing a particular implementation.

At the same time, I do not believe that every decision must be finalized before development begins. On the contrary, some things only become clear as the work progresses. Sometimes, an assumption needs to be tested in practice, feedback needs to be collected from users, or an approach needs to be evaluated under real-world conditions. In such cases, changing an initial decision is not a mistake. It is a natural part of development.

But some decisions have consequences that extend across the entire project. They determine how the system's parts interact, how freely individual components can evolve, how the team distributes responsibilities, and how much effort further development will require. If these decisions are neglected at the beginning, it may later turn out that changing one part of the application without substantially restructuring the others is almost impossible.

That is why I have always wanted to distinguish between the things that genuinely need to be thought through in advance and those that can be refined as the project evolves.

Perhaps this is one of the most significant changes in my approach to development. Earlier in my career, most of my attention naturally went to implementation itself: how to solve a problem, make the program work, and use the capabilities of the chosen technology. With experience, the question of how a decision would affect the product's future became increasingly important.

Not because technical implementation had become less important. It was simply no longer the only part of the work that determined the outcome.

Ultimately, software is not created merely to pass a test once or demonstrate the capabilities of a particular framework. It must solve users' problems, evolve alongside business needs, and remain understandable to the people responsible for maintaining it. If architecture helps achieve all of this, then it is doing its job.

Today, when starting a new project, I try not to rush into choosing technologies. First, I want to understand the problem, talk to the people who will be working on the product, identify the constraints, and discuss how we see its future development. Only then do I move on to specific architectural decisions.

This does not mean that I have a universal blueprint suitable for every project. Rather, over the years, I have become accustomed to asking myself and the team questions that help us avoid overlooking important circumstances or making decisions purely out of habit.

Why are we choosing this approach? What problem does it solve? What alternatives are available? What happens if the requirements change? How difficult will the system be to maintain several years from now? And are we prepared to accept the limitations that inevitably come with our chosen solution?

The answers are not always obvious, and sometimes these discussions take longer than we would like. But I believe that spending time on such questions at the beginning of a project is far more sensible than discovering a year or two later that the system's foundation no longer allows us to move forward without substantial changes.

Perhaps that is why, over the years, I have become more cautious about bold technological promises and universal architectural recipes. I have seen solutions that seemed convenient and justified at the time later create additional difficulties. I have also seen the opposite: how changing an approach at the right moment could significantly simplify further work and free up resources for product development.

I do not want to turn this experience into a set of rules that everyone should follow. Every project has its own circumstances, constraints, and goals. What proved useful in one situation will not necessarily be the right choice in another.

But I am convinced that understanding why we make architectural decisions is no less important than knowing the technical tools themselves. You can know a particular framework inside out, understand design patterns, and be proficient with modern technologies, yet still choose the wrong solution if you fail to understand the problem and its context.

That is why I want to write not only about how particular solutions work, but also about why I arrived at them, what problems I was trying to solve, and which circumstances influenced my choices.

In future articles, I plan to revisit individual projects and examine some of these decisions in greater detail. There will be more technical detail, concrete examples, and explanations of how application structures changed, why the organization of code had to be reconsidered, and how these changes affected subsequent development. Perhaps, in some cases, a different approach would have worked just as well. That, too, is part of engineering: considering alternatives, understanding their limitations, and making decisions based on the circumstances at hand.

For me, this article is an attempt to explain where this way of thinking about architecture came from in the first place.

I did not arrive at it in a single day, and I cannot point to one particular moment when everything suddenly became clear. It happened gradually. Different projects, different teams, different technologies — and the recurring need to understand the consequences of decisions that had once seemed perfectly reasonable.

Over time, these observations came together into a broader understanding: architecture does not begin with diagrams, the choice of a framework, or even the structure of directories in a repository. It begins with an attempt to understand what kind of system we are building, whom it is intended for, who will develop and maintain it, and which decisions genuinely need to be made before a substantial amount of code has been written.

For me, architecture has become a way of thinking about a product as a whole rather than focusing only on its individual features. A way of accounting not only for today's results, but also for the possibility of future development. And perhaps, above all, a way of making deliberate decisions while understanding that every decision has a cost — and that this cost is not always paid immediately.

Sometimes, the consequences only become visible years later. But that is precisely why they are worth considering in advance.

Not to eliminate every mistake or anticipate every possible situation, but to leave the system room to change, give the team the opportunity to work without constantly fighting its own code, and allow the product to continue evolving.

Perhaps that is where architecture begins for me.

Not with the search for a perfect solution, but with understanding why we choose a particular path and where that choice might lead us.


#SoftwareArchitecture #SoftwareEngineering #EnterpriseArchitecture #FrontendArchitecture #TechnicalLeadership #CleanCode #EnterpriseFrontendWorkshop

About the author

  • Lead Software Architect focused on Enterprise Frontend Architecture, Angular, NX, Domain-Driven Design, and maintainable software systems. Exploring practical approaches to AI-assisted engineering with Claude and GitHub Copilot.

Thankx a lot :)

The article illustrates the principles of software architecture exceptionally well—principles that, in my view, truly matter. I wholeheartedly agree with every word. Thank you for this excellent post!