As you have probably begun to recognize, we often need a great deal more than functionality from our software ... and it turns out that most of those other things are also driven (even more than functionality) by architecture. And as we try to address increasingly more of these goals the problem of finding a viable architecture becomes much more difficult ... but being aware of those other goals greatly increases our chances of achieving them.
This paper is a brief overview of the other kinds of problems that architecture may be required to solve.
One might guess that performance is mostly a matter of "algorithmic efficiency". Poor choices (e.g. using an n2 search rather than a logn search) can certainly create problems. But many (if not most) performance problems are caused by architectural decisions. A few examples are:
Similarly one might guess that reliability and availability were the result of well reviewed and tested code. Again, poorly reviewed and tested code can certainly create reliability/availability problems ... but simple bugs are not the only causes of system failures:
If a system is to be buildable with existing tools and skills, then each component must be specified to be implementable within the limitations of those skills and tools. This may greatly limit the range of viable component specifications.
If a system is to be buildable using off-the-shelf technology, the interfaces to those components must be defined to match those of the available technology. If a system is expected to create new reusable components, the interfaces to those components must be designed to meet the needs of future clients as well as the current one.
If we want to enable independent, parallel development of distinct components their interfaces must sufficiently well abstracted as to permit them to be designed independently, and sufficiently well defined that each can be tested for interoperability before the other is available.
If we want to enable continuous integration, each component must have functional interfaces that are easily stubbed or simulated until more complete implementations are available, and the details of those interfaces may well determine the order in which specific features must be implemented.
If we want to be able to gain confidence about the correctness of a component implementation, the component interface specifications must include the ability to generate all behavioral scenarios, and definitively ascertain the correctness of the components behavior in every case.
All of these characteristics are enabled by the system architecture.
When a system misbehaves in the field, it should be possible to diagnose all likely errors (or at least isolate them to a particular component) by looking at a small number of control points. When we think we have isolated the failure to a particular component, it should be possible to confirm this diagnosis by sending test operations through the component in question. This will only be possible if the components and their interfaces were defined with such diagnostic procedures in mind.
When a failure has been diagnosed to a particular component, it should be possible to reset, restart, replace, or update that component without reinstalling the entire system. If a system is to support such incremental repairs, all of the components must have been designed with these procedures in mind.
All of these characteristics arise from the architecture.
Few programs are "write and forget". We will be adding new features to them and adapting them to exploit new platforms and to be used in new ways. If the software has been designed with consideration given to likely changes, these future extensions may be easy. If not, they may be impossible (i.e. extending it will be more expensive than throwing it away and starting from scratch). Unfortunately, as a great commentator on the human condition (Yogi Berra) once observed:
How can we predict what kinds of change will be necessary? Fortunately, many types of change are predictable:
It is good to consider where more general abstractions are likely to be important ... but this can be a trap. Creating layers of abstraction that will never be exploited can complicate and slow down the code for no benefit. Ask yourself: