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.
Development-level benefits
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.
Application-level benefits
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
Single-Responsibility 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.
Open-Closed principle
“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.