Software architecture is the concept of how a software project is structured; a holistic view of the entire system including class hierarchies, interface design, even deployment patterns. A system’s architecture impacts nearly every facet of interaction with that system, from end users to the developers that build and maintain it. When you understand your system’s architecture, you’ll develop an intuition for estimating the time it will take to build new features, where bugs may be hiding in the code, and how to influence the performance of your system. In order to give you the tools to study the architecture of the projects you’ll one day be contributing to, this article lays a foundation for the most important concepts related to the topic, namely architectural patterns, reusability, quality attributes, and tradeoffs.
It is difficult to find a helpful and exact definition for software architecture that accurately communicates its importance and prevalence. One simple definition of software architecture might be “the structure of a software system.” But that definition lacks the gravitas the concept deserves.
More colloquially, you will often hear people define software architecture as “the stuff that’s expensive to change later.” Such a definition, while true, doesn’t really tell us much about what it is. For example, the programming language in which a system is written is expensive to change, but so is the application’s project management software (for example, moving from internal documents to a professional project management system like Jira is very expensive). These are both expensive things to change, but only the programming language is a facet of software architecture.
In truth, software architecture means different things to different people and there is no universal definition that all developers will agree on. To some, software architecture is similar to class design in that it models the relationships between classes throughout the entire system.
Others extend the concept to include not only class relationships, but also the infrastructure upon which the system runs such as the type of database a system might use or the type of web server.
An official definition for software architecture exists in a document from the International Organization for Standardization (ISO) in conjunction with the International Electrotechnical Commission (IEC) and the Institute of Electrical and Electronics Engineers (IEEE) called ISO/IEC/IEEE 42010:2022 – Systems and software engineering – Architecture Description. This standardization document describes software architecture as:
[The] fundamental concepts or properties of an entity in its environment and governing principles for the realization and evolution of this entity and its related life cycle processes
ISO/IEC/IEEE 42010:2022 – Systems and software engineering – Architecture Description
“Environment” in that definition is later defined in the same document as:
[The] context of surrounding things, conditions, or influences upon an entity
ISO/IEC/IEEE 42010:2022 – Systems and software engineering – Architecture Description
Personally, I like this definition and I think it serves the purpose of defining architecture well enough for newcomers to the topic. For the purposes of the rest of this article, however, we will only learn about two facets of software architecture: architectural patterns and components, and reusability. These are the most significant and impactful parts of software architecture as well as the easiest to explain. Let’s go over what each one means.
Architectural Patterns and Components
Architectural components are layers of functionality within a system that are responsible for a specific system-wide task like accessing a datastore or routing application requests. Architectural patterns refer to the ways in which these different components work together to achieve the system’s purpose.
The idea of architectural patterns is somewhat similar to that of design patterns; like design patterns, architectural patterns are made up of components accomplishing specific tasks that are pieced together to accomplish broader tasks in the most effective way possible. Another way in which architectural patterns are like design patterns is that there are many well-documented patterns to choose from, each with their own strengths and weaknesses. However, unlike design patterns, architectural patterns have system-wide impacts.
As an example, consider the factory method design pattern which creates instances of objects based on a given context. This pattern’s components work together to accomplish a very useful task, but outside of that task, it has little—if any—impact throughout the system. While architectural patterns are also made up of multiple components, each with a specific responsibility, the scope of that responsibility is much larger and less specific than those of a design pattern’s responsibility.
It’s useful to think of the components of an architectural pattern as self-contained collections of code, interfaces, and possibly infrastructure. This is a level of abstraction higher than the idea of components in a design pattern because design pattern components only consist of code. As an example of an architectural component, consider the model-view-controller (MVC) pattern. In this architectural pattern, the model component (more often referred to as the model layer) consists of not only class definitions, but also the underlying datastore—like a database—of the application. It’s important to understand this idea of components at the architectural level because components are what make up architectural patterns and influence their strengths and weaknesses.
Reusability
On the topic of software architecture, reusability refers to a component’s suitability for handling a specific system-wide task. To use the MVC architecture example again, the architectural component called the controller layer is responsible for handling application routing tasks. Most of the time, MVC is used in web applications, so the controller layer is responsible for handling HTTP/HTTPS requests from browsers, sending them to the appropriate code hosted on a web server, and sending the response back to the browser. Once the controller layer is built, we don’t have to write it again. This means that no matter how many features we have, the controller layer is reusable for handling all the application routing needs of that feature. This saves the development team weeks of time by sparing them the task of writing HTTP routing code every time they want to introduce a new feature into a system.
By now we should be starting to understand what the concept of software architecture is getting at. Now, let’s talk about why software architecture is worth knowing about.
Why Architecture Matters
Software architecture is the “big picture” of a software system and therefore permeates every aspect of the code. Consider an obvious metaphor: the architecture of a building. The building’s architecture dictates everything about the building that makes it useful; how many people it can hold, where the emergency exits are, how to move from one area to another, the functions of different parts of the building, how future contractors might make upgrades to the building, even how the building affects the environment around it.
Additionally, a building’s architect has to make decisions about what attributes of the building are most important, which are nice to have, and which are not important at all. For example, an architect may be told to make a building with the primary goal of keeping operational costs low, even if the building isn’t very aesthetically pleasing. In this case, the architect will decide that, even though floor-to-ceiling windows on every floor would make for a very nice-looking building, it would be terrible for operational costs because the sun will heat up the building during the day and the people inside will have to turn up the air conditioner. In this case, the architect will have to opt for smaller windows throughout the building and perhaps even exterior walls of concrete to absorb the heat.
Likewise, software architecture dictates everything about the system that makes it useful, and software architects have to make decisions that sacrifice one thing for another. In software architecture, these “things” that are sacrificed are called quality attributes, and the decisions about which quality attributes to prioritize are called tradeoffs. Let’s talk about both of these concepts, starting with quality attributes.
Quality Attributes
Quality attributes are the non-functional attributes of a software system. In this case, an attribute is non-functional in that it doesn’t do anything. For example, suppose we’re building a calculator app. Being able to do addition is an example of a functional attribute. On the other hand, how quickly the calculator can produce the results of adding two numbers together is a non-functional attribute. Saying the calculator is fast or slow is a comment on one of the calculator’s quality attributes. Aside from the power and modernity of the physical infrastructure upon which a project runs, nothing impacts a system’s quality attributes more than its architecture. Let’s define some of the most important quality attributes, what they are, and how architectural decisions affect them.
Reliability
Reliability is a quality attribute that defines a system’s ability to perform its tasks under some given conditions for some given amount of time. For example, suppose we’re building a web server upon which someone to host their website. Say one of our users has their homepage, index.html, on our server. We expect that any time someone in the world sends and HTTP request to our server requesting that index.html file, our server will properly send the page back to that person. Occasionally, due to any number of reasons, our server will fail to send the requested index.html page and will instead send the user a 404 – Not Found response code. This is considered a failure.
The probability of our server correctly returning the requested index.html page at any given time under some given condition is a measure of reliability. For example, suppose we send one thousand requests for the index.html page over the span of fifteen minutes and our server correctly sends back the index.html page 991 times, the reliability of our server with at one thousand requests over fifteen minutes is thus 99.1%.
One way in which software architecture affects reliability in this case is how we decide to handle request failures on the server. When the server receives a request for index.html and for some reason cannot find it, instead of sending back a failure 404 message to the user, we could tell the server to try again up to five times. A very simplified version of how this might look in the code is as follows:
def handle_request(file_name, retries=0):
try:
resource = open(file_name)
# File was found, send it to the requester
send(resource)
except:
# File not found, try again
retries += 1
if retries < 5:
print(f"Resource not found, retry number {retries}")
handle_request(file_name, retries)
else:
# Couldn't find file after 5 attempts,
# send 404 error message
send(404)
In the code above, we wrote a function that handles a request for a resource like index.html and sends it back to the user (note, the send
method is not defined in this code, this is just for example purposes). We put the open
method inside of a try
block and use the except
block to handle any failure to find the resource. This will help alleviate any random failures that might happen by simply telling the server to try again. However, we also prevent any infinite loops from happening by telling the server to quit trying to find the requested resource after five tries. If the max number of retries is reached, the server sends the 404 response code. Now if we were to test the server again with a thousand requests for index.html, we may see improvements; perhaps the server eventually sends the requested resource 999 times, increasing our reliability measure to 99.9%.
Availability
Availability is closely related to reliability in that it measures the probability that a system will be available to perform its task at any given time. This measure is directly impacted by the system’s reliability, but also takes things like maintenance downtime into account. Availability over some period of time is measured as that period of time, minus the amount of downtime and then divided by the period of time being measured. For example, if we measure daily availability and find that the system is unavailable for about five minutes every day, the availability of the system is 1440 (the number of minutes in a day) minus 5 (the number of minutes the system is unavailable), all divided by 1440, or 99.65%.
You may occasionally hear people refer to their system availability in terms of nines. The system in our previous example has “two nines” of availability because the number 99.65 has two nines in it. These numbers are often used in service level agreements (SLAs), contracts that system developers have with their customers agreeing on how much availability they can expect from the system. Five nines (99.999%) of availability is considered very high availability as it allows for five minutes and 15 seconds of downtime a year.
Software architecture impacts the system’s availability by defining how system maintenance is conducted. For example, if an application’s database goes offline for some reason, can the application still be accessed while we figure out how to get the database back up and running, or does the whole system have to come offline? If we wrote our components to be reliant on the database, the whole application will likely be unavailable while the database is down. If it takes us five minutes and 15 seconds to get the application back online, we can’t have any more downtime for the rest of the year if we want maintain five nines availability.
Scalability
Scalability refers to how a system’s performance and cost increases and decreases with demand. For example, imagine our system has a baseline number of 500 users at any given time, but for some reason, one day we have two-thousand users for a couple of hours. In order for the system to perform identically for all users, we’ve programmed it to spin up a duplicate server every additional 500 users to handle the extra demand. Also, we’ve programmed the system to spin down those servers as the spike in demand tapers off. This means that both cost and performance increase and decrease based on the demands placed on the system.
Scalable systems are created by writing modular code. As a senior engineer or the architect on your team, you enforce code modularity by ensuring that the components that rely on infrastructure can handle that infrastructure being duplicated. For example, hardcoding the IP address of our servers in the code would not be conducive to scalability since new servers would have different IP addresses.
Security
Security is a quality attribute that refers to a system’s ability to protect data from unauthorized access, prevent users from accessing a higher level of privilege in the system than they need, and much more. Security is a broad topic and there’s a ton of information to know about it, so this section will cover only a very small subset of how it applies to software architecture.
When you’re architecting a system, you have to think about how data will be stored and if it’s allowed to travel networks unencrypted. For example, when transmitting data from a web server to a user’s browser, the data is accessible to people (network administrators, people using packet sniffers, and so on). In fact, it’s good practice to assume that any data traveling over a network is being read by nefarious people. We might not care who sees some data if it’s something innocuous like a person’s shoe size, but often we transmit data like names or credit card numbers over a network and we definitely don’t want people reading that, so we encrypt it.
**_Encryption_**, like security, is also a broad topic but I wanted to briefly define what it is in case you’ve not heard of it before. Basically, encryption is the process of scrambling data so that it’s not human-readable, and the only way to unscramble it is with a special decoder called an encryption key. Generally speaking, only the machine receiving encrypted data has the appropriate encryption key, so even if that data is captured by nefarious people, it is no good to them because they can’t read what it says.
The system’s data encryption policy is an important architectural decision because data is much bigger when it’s encrypted than when not. As a consequence, encrypted data takes slightly longer to travel from point a to point b in a network. It’s not a huge difference, but as your system grows, the amount of data flowing through the system will impact the system’s performance. Figuring out what data needs to be encrypted is just one small facet of security minded software architecture.
Maintainability
The last quality attribute we’ll discuss is maintainability. A system’s maintainability is a qualitative measure of how easily developers can add features, fix bugs, or tweak performance. Maintainability isn’t something we can measure with a formula; it is more of a general feeling about how easy the system is to work with from a developer’s perspective. Many of the topics you learn as you become a more senior developer are techniques for writing maintainable code. For example, making smart decisions about class design makes future work easier to do because intuitive class design lowers cognitive complexity. Likewise, using design patterns where appropriate along with naming their components descriptively helps developers navigate the system easily, reducing the time needed to add a new feature or fix an overlooked bug.
Software architecture impacts the maintainability of the system because every architectural decision influences how code must be written. As an example, a microservices architecture pattern is useful for separating business concerns into individually deployable components. This means that, as a developer, if I get tasked with writing a feature for the marketing team, I know exactly which component I need to work on. However, if I’m working in a system built on the MVC architectural pattern, I may have to work with several components to deliver the required feature.
Architectural Tradeoffs
Now that know what quality attributes are and have seen some examples, let’s talk about architectural tradeoffs. As a senior developer or software architect, you’ll often be faced with building features that have competing priorities. Consider the example we used when talking about reliability. Adding the try … except
blocks to the server’s code allowed us to increase the server’s reliability, but it most likely will make the server a little slower. This is because exception handling is very slow when exceptions are raised, meaning that every now and then, a user will have a slightly slower experience than if we got rid of the server’s exception handling altogether.
Deciding which is more important, the speed of the request response or the reliability of the server, is an architectural tradeoff you will have to make. This decision will be influenced by many factors such as the kind of application you’re building, the needs of the users you’re building it for, and much more. There are no universally correct answers to architectural tradeoff questions, they always depend on the context in which you’re working.
One of the tradeoffs you’ll have to think about quite often is security. As we discussed in the quality attribute section, something like encryption can have a significant impact on your system’s performance. As an architect, you’ll have to decide what needs to be encrypted and what doesn’t. If you’re working in the defense industry, you’ll likely have to encrypt every bit of data that travels over a network, causing your system to perform slowly. However, if you’re writing web-based browser games that don’t require a login, you likely don’t need to encrypt anything. The kinds of tradeoffs you make will depend on the industry in which you’re working and the type of system you’re building.
One thing that will impact your architectural decisions and tradeoffs is the due date of your projects. For example, if you have a year to build a back-office application for a small company, you will likely have maintainability as a high priority so that you can troubleshoot problems easily and add new features to the system upon request. Conversely, if you’ve been tasked with building the same system in a month, you will not take the time to consider maintainability. This is because building maintainable systems requires extra care and planning, time you don’t have with such a quick due date.
Yet another decision about architecture you’ll have to make is whether to build a component yourself or use a third-party component. Third party components, sometimes called commercial off the shelf (COTS) software, are components built by other organizations that you usually have to pay for. COTS software is supposed to require very little work for a developer, and sometimes includes 24-hour support (that is, you can call someone for help if the COTS software isn’t doing what you want it to). One example of a homegrown vs. COTS solution is the decision on how to monitor your system’s performance. Quite often, it’s necessary to watch your system’s performance such as CPU or RAM usage, network latency, and much more. Some teams find it best to write their own software for monitoring system performance while others buy performance monitoring software from other vendors. The decision on which way to go will depend on a lot of things such as staffing (you need more developers to build and maintain such software) and cost (some performance monitoring applications are very expensive), and whether or not the COTS software can easily service your needs.
Finally, perhaps the most important consideration when making architectural tradeoffs is cost. You and/or your company have a finite amount of money set aside to build whatever system you’re working on. How you use that money to accomplish the goals of the system often includes making several architectural tradeoffs. For example, do you want to pay for a database management system like Microsoft SQL Server so that you can have 24-hour support ready to help you if something goes wrong, or do you prefer using an open-source option like MySQL or Postgres for free, but which do not have a helpdesk for you to call? The answer to that question will depend on many factors. For example, are you a lone developer for the company you’re building this system for, and the company needs the application to be running all the time? If so, you might want to opt for the SQL Server option since you may need help one day. Conversely, are you building an open-source project for people to share recipes with each other? If so, your budget is probably small, and your users won’t lose millions of dollars if your database stops working. In such a case, self-hosting a free database system like MySQL should be fine.
The responsibility for making the decisions on these tradeoffs will fall on you more often as you become more experienced in whatever tech community you’re a part of, be it an open-source organization or a company. Understanding the overall goals your system and how your organization plans to use and support it will be an important step towards making good architectural tradeoffs.
Conclusion
This article introduced an important concept in your path to becoming an experienced and professional-grade programmer: software architecture. Software architecture can be defined in a number of ways, but it’s ultimately the process of developing components and fitting them together in the most efficient way relevant to our system’s goals. We learned about those components and how they form architectural patterns. Then, we learned about quality attributes and how they’re impacted by software architecture. Learning about quality attributes is a prerequisite to learning about architectural tradeoffs, the decisions you must make when developing systems. Hopefully now you have an understanding for what software architecture is and why it’s important.