The Role of Architecture
The tech stage has erupted in the last decade. We now work on some of the most complex systems ever. As users, we need the flows in our applications to be fast. Almost all our actions happen on our phones or online, from socializing and entertaining ourselves to paying bills and making medical appointments. At the same time, many of the applications we use are monetized, so we need features to create the best content. On the other hand, from the cars we drive to the houses we build, everything has some features that implement AI, IoT, or some kind of application that automates specific actions. From the database to the code, from functionality and user interaction to how systems are organized, we are faced with challenges at every step in the development process of a product. More than ever, it is essential to understand what we have, what we can build with our information, and what our end goal is. This is how the need for a structure arose, and it became increasingly important to create systems that could evolve while maintaining the balance between business and tech requirements.
In this chapter, we will cover the following topics:
- What is architecture and why is it so important?
- The impact of architecture
- Understanding the role of stakeholders
What is architecture and why is it so important?
If you ask a group of developers what kind of architecture works for them, you will receive many different responses reflecting each person’s experience. Architecture is a term used to define many structures, but we often hear about it within the construction domain. The parallel here extends beyond the use of the noun itself to the common fact that, as we will discuss in Chapter 5, Design versus Architecture, structure, and design, whether for software or a building, are the results of the requirements of different stakeholders.
Explaining precisely what software architecture is or does is hard. Still, luckily, an at-hand comparison that gives us some perspective on the impact of an architectural decision is building architecture. Just think about how hard it is to change the architecture of a finished construction. The same goes for software. Even if the “damage” is not as visible as it would be when tearing a house apart and building it again, the implications are the same: time, finances, and an impact on different areas and stakeholders.
Can we define software architecture?
The debate always comes down to one question: “What is software architecture?” Although it is a highly discussed and important matter, there is no definitive and generally applicable definition. While trying to shape a meaning as thoroughly as possible, I have found many interpretations and explanations, many confusing and hard to tackle, while others were simple and to the point. For me, the subject of architecture started to make sense when I started working on a project as a full-stack developer. As I had to go through different layers in the application (working from the database to the client), I had to respect the rules that this architecture imposed. Then, step by step, by becoming more curious about different technologies and approaches in other projects, I discovered that this is an exciting and essential matter that I had to explore fully. So, to gain some clarity, let’s take the first step by discussing some existing definitions and perspectives, and then we will try to shape a report.
According to Ralph Johnson, “architecture is about the important stuff. Whatever that is.” This is a very abstract definition but may be a good representation of how important the context of the project is when determining the architecture.
Another exciting perspective I found from Eoin Woods states that “software architecture is the set of design decisions that, if made incorrectly, may cause your project to be canceled.” Though radical, Eoin Woods’ definition exemplifies how important it is to always ensure we have explicit requirements when making decisions about a system’s structure.
The list of examples can go on; however, my conclusion was that many seemed pretty abstract and ambiguous for a developer, especially one in the early years of their career. From “the process of defining a structured solution that meets all of the technical and operational requirements” to “the set of structures needed to reason about the system, which comprises software elements, relations among them, and properties of both,” each of them yet again simply underline my idea that defining architecture would create constraints and possibly many debates. What if software architecture is a set of characteristics to pay attention to while expressing a direction on which we build step by step?
From my experience, the simplest way to think about software architecture, no matter the level of experience, is this:
Software architecture is a set of characteristics that combine technical and business requirements (at the project and organization levels). You will be able to define a skeleton to help you build a performant, secure, and easy-to-extend system (or application).
So, it is a structure upon which we build step by step, one component after another, creating the relationships between them. Consider that all this is being realized while considering the bigger picture of the product.
Even though we talk about components and interactions, we must be careful with the level of abstraction. We can quickly get too absorbed in the implementation details and lose our view from where we are to where we want to go within our application development map. There is a fine line between software development and software architecture. In my opinion, and from the experiences I’ve had within the teams I’ve had the chance to interact with, I can say that it is obvious when an architect is too focused on abstractions and has no idea about what is happening in the application. At the same time, I can see when a developer lacks awareness of the software architecture.
We will debate an architect’s involvement in the development process in Chapter 6, Types of Architects and Their Focus. That’s why right now, I would like us to focus a bit more on how software architecture knowledge can help us write better code.
Among all the questions we address when talking about software architecture, two can significantly help in the development process: “What are the main components the application can be divided into?” and “How do these components share responsibility?”
Understanding how the system is divided into components and how they interact brings much greater clarity to the process of defining the responsibilities of each, seeing where best practices integrate, bringing value to reduce the interdependence of different parts of the code, and easing the process of creating unit tests. At the same time, we have a huge advantage because it will become easier to identify frequently used components, create a common ground, and apply the correct patterns.
It is easier to respect best practices and choose suitable design patterns and abstractions when we understand what we are building and why certain decisions were made at the architecture level. Everything works together beautifully and we find it easier to build a quality-oriented product (a product that respects the quality attributes set at the birth of the product).
Understanding that my team needs an overview of the application’s architecture was a stepping point in reconsidering how certain pieces of the system interact. This is extremely useful, mainly when we discuss complex team organization with dedicated subteams. It’s hard to keep track of how all the components interact, define precise requirements (especially for the teams we are dependent on), and create pieces of components that fit together when there is no overview.
All in all, if the structure of the system is built the right way, we will have a successful product. If not, at some point, we will have to start over or rewrite essential parts.
When I’m referring to the base, I particularly consider two directions:
- Architecture is the foundation of the application. We already discussed this; it is as though we were building a house. It has to allow you to create the walls, the roof later, and other details that you might want along the way or as trends evolve and change, but above all, it should give stability and direction for how the rest will grow at a structural level. The architecture is a system’s base and must be carefully thought through to bypass any significant design changes and code refactoring later.
- On the other side, the development team is also the base. We can have the best architecture plan, but if the ones building it step by step don’t know what they’re supposed to be doing or don’t have an overview to support this, we will come into a lot of trouble.
A slight improvement at the code level to support the system’s architecture or design will have a considerable impact over time.
An important aspect to consider when it comes to architecture is the fact that it has to be shaped around requirements of significant impact. Good architecture is not one with fixed definitions and limits but one in which technical needs and business requirements are aligned and work well together. I was part of a team where the development team was composed of great developers, but the requirements were dramatically changing, and this made it hard to have continuity in the product life cycle. We had to constantly change the structure, rewrite parts, and had a lot of technical debt to take care of.
Another critical aspect is that the architecture should focus on the high-level structure, independent of the implementation details. Of course, implementation details are being decided along the way and are not to be ignored, but they shouldn’t impact the structure; instead, they should build upon the structure.
From the start, it is essential to know that we have to be as pragmatic as possible and consider as many scenarios as we can when deciding the architectural shape. Since we are discussing strategies and edge cases, an excellent way of being as precise as possible about these is by exploring and analyzing the future steps and plans with the stakeholders. Stakeholders are critical and of great help in building a valuable product by providing feedback. In the Understanding the role of stakeholders section of this chapter, we will discuss the main stakeholders and why they impact the application.
So, we agreed that it is hard to define architecture, and we saw that what needs attention is determined by the context of the product you are building. At the same time, even though we can provide a clear definition, we can still learn best from the experience of others and look at some points that, in time, show themselves to be of great value.
Often, we might feel as though architecture is all about technical requirements and concerns, but the actual value of a project is given by the functional requirements and the validation of stakeholders. You can have the technically best product with the best architecture and components, but if no one uses it, all that effort is in vain. One of the best skills an architect can build is balancing the functional and non-functional requirements.
Working closely with the various stakeholders as possible helps us correctly identify, understand, and refine our requirements. Ultimately, the end software architecture solution will be defined by those requirements. This is part of the context we have already mentioned. Suppose the system architecture is defined too much by the business context and we make a choice that cannot meet all requirements. In that case, we will end up with anything but a solid architecture that can later be extended or even implemented.
Maintainability and extensibility
System maintainability is the subject of many books but is still very subjectively defined in many cases. This property determines how easily you can change, refactor, extend, or repair your software. Software maintenance is an ongoing process, so it’s essential to be prepared to use the least amount of resources possible to make this possible.
Your architecture should be flexible enough to allow you to work with requirements later that you were not aware of in the beginning. As an architect, even if you don’t have 100% of the details in place, to be safe, you have to at least think in terms of the evolution of your system. Predict possible risks and find ways to avoid them.
But I have a disclaimer here, which also applies in the development process: don’t over-engineer scenarios if you don’t know whether they will ever even happen. When I mention that we need to prepare for change, I refer to respecting best practices, making the code easy to work with, and extending and modifying, rather than predicting what might happen in the future. Here, we can turn to the You Ain’t Gonna Need It (YAGNI) concept, which represents incrementally creating a simple system design. When you find yourself saying, “Maybe the client, user, or product owner will want this functionality in the future, so I will also code for that scenario,” stop for a second, and acknowledge that you are overthinking and that you’re about to code for a scenario that will never happen. Instead, evaluate how testable, clean, and extendable your code is and move on.
Response to change
As business evolves, so does software; it is a natural consequence. Technology evolves, business perspectives change, technical debt appears, requirements are reconsidered, team structures change, and the need for a more performant technology stack arises. What we need is to think in terms of building tolerance to change in the system. Observe where and what kind of changes appear through iterations. You can expect these changes and be prepared to meet them. Why so much analysis? Because every time we assume a final decision, we are also saying no to other details we might receive along the way, which will help us make a more informed decision. Once you have committed to a way of shaping your architecture, you have already decided how the next period of time will be spent and how the product and the architectural structure will evolve.
James Clear aptly defines the importance of decision-making by stating that “when you say no, you are only saying no to one option. When you say yes, you are saying no to every other option. No is a decision. Yes, it is a responsibility. Be careful what (and who) you say yes to. It will shape your day, career, family, life.” This is also valid when it comes to software development decisions.
Take care of how the pieces of the system interact, reduce complexity and independence as much as possible, and identify the essential components and how they are being implemented. This way, even if you have to change something along the way, the changes will be isolated, and a complete architectural remake can be avoided.
Defining architecture is not just a preliminary stage; it is a continuous process. In this case, the architect is responsible for having a clear perspective on how the system evolves and if the decisions made at the beginning are still valid. At the same time, the software architect must ensure that everyone on board understands how the system works. Whether we like it or not, systems become more complex as they evolve. This complexity can sometimes be hard to observe or tackle early on. When we start losing our understanding of what we are building, we end up with a huge problem. As the system becomes harder to understand, it becomes tedious to reuse some parts, stick to the design decisions, maintain it, or extend it. As the team structure changes, the learning curve for new members increases.
Looking back at the history of software architecture, we notice a shift in creating this timeline of decisions. In the beginning, architectural decisions were, without exception, made in the early development stage, seen as important and hard-to-change decisions. Later, with the rise of agile and the philosophy of working with iterations and being open to change, the approach changed, and the conclusion reached was that the process of shaping a system should be smooth and in sync with change. So, the emphasis was placed on how open to change, extensible, and maintainable the system was in reality.
Let’s say we defined and agreed upon requirements with the stakeholders; so, what’s next? Another significant step that needs to be done as early as possible in the life cycle of a product is considering quality attributes. You know you have explicit quality attributes when they shine through the application. You don’t have to wait for it to be developed or deployed; the architecture should speak for itself. By defining quality attributes, we become more particular about whether we can meet all the requirements or not. For example, by thinking about performance at every step of extending the system, we end up with a performant application because, for example, we will consider what change detectors we have from the perspective of a frontend developer, in the client and where, how many times we load a page, or how many calls an API can handle. A quality attribute guides us. Later, it might become complex and time-consuming to make impactful decisions and changes in these areas.
The product quality model defined in ISO/IEC 25010 comprises the eight quality characteristics shown in the following figure:
Figure 1.1– Quality model (source: https://iso25000.com/)
Next, we will discuss how architecture is more than technical decisions and how considering the context in which we build the product will help us make better decisions.
The impact of architecture
Because we can’t change an architectural decision in an afternoon, the first step in architecting software is understanding what is significant and why. When talking about substantial matters, we talk about concepts such as the technologies chosen or the high-level structure, understanding how to tackle risks, and controlling the complexity we add to the big picture as we make changes. An excellent example of a significant decision I had to work with was how to switch some applications from AngularJS to the new Angular when AngularJS became unable to comply with certain UI or UX requirements. Or, when perspective changes were made, many applications wanted to break the monolith and switch to a microservices-oriented architecture. These are changes that need a high degree of effort and resources.
It’s easy to think that if you are writing good code and following some well-tested approaches, keeping an eye on the architecture is not essential. However, there are some benefits to getting the bigger picture, from understanding how components in the system should interact to which should have what responsibility and the cost of what you are implementing as a developer. Having an overview of the architectural dynamics helps us answer radical questions such as the following:
- Which are the main components of my system?
- How do these components interact and evolve?
- What resources will I need and what costs will I have in the development process?
- What are the areas where I can predict change?
Another important metric is the ability to predict change. Darwin’s theory says that the species that survives is not the strongest but the one that adapts better to change. If Darwin had worked in software development, I think he would have stated that the system that survives longer is not the strongest but the one that has an architect that can predict and adapt better to change.
Often, we encounter the idea that development teams should focus their attention on coding and not bother with architectural concepts since those aren’t their primary concern. This is an incorrect way of looking at things because there are so many benefits to looking at what you are working on from above. For example, the moment I stepped away from the IDE because things were not making sense, I started to understand what I was building and came up with better solutions instead of complaining about the same problems I didn’t understand before. Working toward consistency between the process of translating features to code and the process of defining best practices, I could see where some technical guidance might be needed and visualize component interactions from a higher level. It changed my perspective on my daily work.
I firmly believe that the architect’s perspective can be flawless. Still, if the team implementing the architecture does not understand what they are building, that is a big red flag regarding the team dynamics.
Some of the matters that improve at the development level when creating an overview of what we are building are as follows:
- Consistency regarding the way we implement features: align when starting to work on a new part, have knowledge-sharing sessions if needed, undertake pair programming, and brainstorm how to solve certain problems.
- Consistency regarding code quality: everyone should be aware of the guidelines and best practices and apply them consistently to grow a healthy system.
- It’s easier to evaluate the team technically: when we know the practices we follow and test, it is easier to spot the areas where something is not okay. It is easy to note in the team when and if someone does not understand what they have to do.
- It’s easier to identify the specific points where technical leadership is necessary.
- Same overview and vision: the whole team works toward the same objectives when the objectives are clear.
- Validation for the whole structure: architecture is being implemented through code. If we don’t write quality code backed up by best practices, the consequences will be reflected in how the architecture evolves or can’t evolve. The reverse is also valid. Best practices and excellent developers can’t do much with bad architecture. Potentially reuse some parts when rewriting. It is healthy to identify this kind of matter, align, and have everyone on the same page.
We understand how all the pieces work together, how they will evolve in relationship with one another, and why it is essential to respect quality guidelines. At the same time, the team is motivated to work on themselves and improve their skills to build a high-quality product.
When discussing requirements, there will always be a definitive list of what people want a system to do. Alongside this, a plan split nicely between features, user stories, flows, and even tasks will always exist.
Some requirements are not understood very well and are requested just because they are popular in the market. We don’t know what they mean in detail in terms of implementation, but we know they are essential for “performance” or “security.” I have seen this situation in architecture workshops. When talking about the well-known “quality attributes,” everyone wants them, but they aren’t all equally important in the product context. They are, at the same time, hard to implement all at once, and they must be prioritized. It is like someone saying that from today on, they will exercise daily, eat healthily, read 50 pages, and meditate. It’s impossible to enact all of this at once. You need to take it step by step, see what impacts your life most, and add more improvements along the way. The same goes for architectural quality attributes. We need to check the most important ones, how they influence each other, and what metrics we need to ensure they have the desired results and impact.
Some are more relevant and have more weight depending on the context of your system. One of the best approaches when you don’t know what you want from a system is to list what you don’t want.
Some results of bad decisions could be the following:
- Complexity – your system is hard to work with, hard to understand, and hard to explain. Always check that the code you write is easy to understand and that the design and architectural decisions you make are clear to everyone involved in the development process. Make the system easy to understand.
- Not being able to test – if you can’t test your system, it’s because you have too much coupling or some of your components do too much. Create modularity so that your layers and components can change independently without impacting other parts. Create tests to track when changes negatively impact other areas. Also, if you want to extend the system without testing it at every change, having considerable coverage is a safety net.
- Unmaintainable and hard to extend – this is a toxic circle. An untested system is a system predisposed to increasing complexity; complexity makes the system hard to understand, so it becomes tough to extend, maintain, or even refactor.
- Fragility – with every new thing you add or fix, something that seems completely unrelated breaks. This makes it very hard to be productive, takes a lot of time to investigate, and takes a lot of testing to gain back some control.
We will go into further detail about the approaches, principles, and ways of controlling the quality of our architecture in Chapter 4, Discussing What Good Architecture Is, but first, it’s essential to give some shape to the way architectural components should interact and evolve by discussing some of the must-know principles of every developer: the SOLID principles.
SOLID represents an acronym for some of the most popular design principles. These principles can be implemented at any level of code complexity and are intended to make software maintainable, extendable, flexible, and testable. The principles were promoted by Robert C. Martin (Uncle Bob) in his 2000 paper, Design Principles and Design Patterns. The SOLID acronym was introduced later by Michael Feathers.
Uncle Bob is also well known as the author of Clean Code and Clean Architecture, so these principles are strongly tied to clean coding, clean architecture, and quality patterns.
One of the main benefits of these five principles is that they help us to see the need for specific design patterns and software architecture in general. So, I believe that this is a topic that every developer should learn about.
Let’s look at the five principles in detail:
- Single Responsibility principle
- Open-Closed principle
- Liskov Substitution principle
- Interface Segregation principle
- Dependency Inversion principle
“There should never be more than one reason for a class to change. In other words, every type should have only one responsibility.”
The first principle refers to the fact that each component, at any level – including the architectural level – should be thought of as having only one responsibility, one unique matter to resolve, and a single reason to change. This principle also represents massive support for the maintenance process because it helps with the following:
- Avoiding coupling. Coupling indicates how dependent on or independent of one another components are. High coupling is an issue.
- Creating small, independent structures that are easier to test.
- Shaping a system that is easier to read and understand.
“Software entities ... should be open for extension but closed for modification.”
The systems we build are ever-changing due to shifting requirements and technological evolution. Having to deal with so much change and having to extend or maintain systems helps us along the way to gain experience and lessons to make better future products. One of the lessons is precisely what this principle stands for: entities and components should be independent enough so that if the need for change appears in the future, the impact on existing structures is as minimal as possible.
Liskov Substitution principle
“Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.”
This principle is one of the hardest to understand, but we will dig deeper into the technical details later. For now, and in the context of architecture components, let’s keep in mind that this principle simply requires that every derived component should be substitutable for its parent component.
Interface Segregation principle
“Many client-specific interfaces are better than one general-purpose interface.”
The I in SOLID stands for more than the I in Interface – it stands for an attitude, a skill that comes with experience. This principle states that we should always be careful and split significant components or interfaces into smaller ones. This principle works very well with the S from SOLID because we can determine whether an element has more than one responsibility and needs splitting.
Dependency Inversion principle
“Depend upon abstractions, [not] concretions.”
The principle of dependency inversion refers to the decoupling of software modules. Understanding this principle is valuable because it helps us to see abstractions. High-level components should not depend on low-level features; both should depend on abstractions. As a result, the changes we make in the higher-level components won’t impact the implementation details.
Understanding the role of stakeholders
Being part of the team and being focused on development can take the focus away from who is impacted by our work. We talk so much about user needs and experience that we cannot look from above and see all the parts affected by our product. Until the moment of being used, systems must be shaped, built and tested, may have to be extended, and are usually maintained, planned, and financed.
The truth is that stakeholders have a great deal of decisional impact on the product’s evolution and we must pay attention to their needs. The stakeholders are the ones from whom the demand for a product appears. You must identify your stakeholders, keep them close, understand their needs and perspectives, and create an architecture that meets their requirements as effectively as possible.
Software development, and software architecture even more so, can become very complex. Also, as we already discussed, software architecture is tremendously impacted by requirements, and those requirements come from people that are not necessarily technically minded; so, in this case, it is an architect’s job to make sure that everyone understands what we are building. This way, we ensure everyone knows the product’s direction and structure and can keep track of its evolution and changes. Having everyone on the same page can provide valuable feedback and help us make better-informed decisions.
- Before getting to the users, analyze who is part of building the product, such as the project management team, development team, and designers.
- Look at who is using your product, such as the customers and users.
- There are also people not directly involved and not using the product but who are part of the process by facilitating some actions and techniques, such as top managers and company owners.
Software architecture is a set of characteristics that combines both the technical and business requirements, with which we will be able to define a skeleton that will help us to build a performant, secure, and easily extendable system.
Software architecture is not a one-time decision but a part of a continuous process.
A successful architecture builds a system used in real-life scenarios by real users and is validated by all relevant stakeholders. You can have the best system technically, but if it does not fulfill the needs of stakeholders, your effort is in vain.
Stakeholders are not only just users and should be part of the architecting process. Identify stakeholders, try to understand what they need, define the precise requirements, and implement them using quality attributes.
The next chapter will examine some popular patterns used in defining the architecture.