Making a decision implies we’ve seen multiple options from which to choose. If there is only one option, then we didn’t decide anything; the decision was made for us. To ensure we see many options, we need to explore the design space.
Design exploration is an iterative journey of divergence and convergence. Once we’ve identified a problem, we diverge our thinking and generate design alternatives that can solve that problem. Once we have a few options on the table, we’ll converge our thinking by building consensus and eliminating options that are a poor fit for the current problem.
Human brains crave options. Our confidence in a decision increases after seeing multiple alternatives. Unfortunately, there isn’t time to explore every possible option and all aspects of a software system’s design. Architects need to focus on and champion quality attributes, structural organization, and design decisions that will influence these things.
Grady Booch has said, “All architecture is design, but not all design is architecture.”[5] As you learned in What Is Software Architecture?, a system’s software architecture is the set of significant design decisions about how the software is organized to promote desired quality attributes and other properties. Architects must explore these significant design decisions and actively choose how to organize the software to achieve desired quality attributes.
Here are areas of a software system’s design architects will typically explore:
Recall from Define the Essential Structures that structures in the architecture are made up of elements. In a well-designed architecture, every element has clear responsibilities. Any element without a well-defined responsibility should be eliminated. Exploring design options requires that we explore combinations of elements with varying responsibilities.
Relations describe how two elements in the architecture work together to accomplish a task. A component’s interface is one example of a relation. Both the communication mechanism (for example, HTTP, TCP, or shared memory) and the rules for communication (such as APIs, response objects, or required data) define the interface. The rules governing interfaces and element communication are inherently architectural, at least up to a point. We can defer some details—such as method names and sometimes the fields returned in a response—to downstream designers.
Every problem has its own terminology and concepts, which describe the world in which it exists. The concepts from the domain, be they objects or events, must be accounted for somewhere in the architecture. The better we understand the problem domain, the better we’ll partition elements and assign responsibilities to them in the architecture.
Modern software development technologies are loaded with architecture assumptions. Frameworks, middlewares, libraries—any off-the-shelf technology—comes with attitude. The technology will tell you how and when to use it. Opinionated technologies force decisions on to the architecture.
When the technology aligns with our needs, then life is rainbows and unicorns. When our needs fall outside the bounds of what the tech thinks we need, then prepare for a battle royal between you and the framework.
How we design the architecture influences how the software is constructed and deployed. If we desire continuous delivery, if we want to have multiple developers working in parallel, if we require the use of specific testing strategies, then we must design the architecture to support these requirements.
All design is redesign. Most architecture explorations start by looking at what we already know about how to design software. We can codify design knowledge as a rule of thumb or a documented pattern. Knowledge can come from your own experience or as legends passed from architect to architect over the ages.
Since we want to create a clear connection between our design decisions and stakeholders’ needs, we’ll use the categories of architecturally significant requirements from Chapter 5, Dig for Architecturally Significant Requirements to organize our approach to exploration and decision making.
By Len Bass, independent consultant and co-author of Software Architecture in Practice [BCK12], Documenting Software Architectures: Views and Beyond [BBCG10], and DevOps: A Software Architect’s Perspective [BWZ15]
One of the most easily overlooked items when designing a system or service with multiple instances during execution is deployment. There are two basic methods for deploying a new version of a service with multiple instances: red/black or rolling upgrade.
A red/black deployment (some names use different colors like blue/green) allocates sufficient virtual machines for all instances of the new version, deploys the new version into those instances, and then switches to use the new instances. A rolling upgrade will upgrade one instance at a time.
In either case, there are possibilities of inconsistencies. For example, suppose you have a chain of services—Service A depends on Service B, which in turn depends on Service C. Now one of your developers deploys a new version of Service B. This new version may change the syntax or semantics of the interface. What happens when Service A invokes Service B and gets an incorrect error because the semantics of an interface has changed? What happens when the new version of Service B assumes a new version of Service C, and the new version of Service C has yet to be deployed?
If you are deploying new versions using a rolling upgrade strategy, then it is possible that two different versions of Service B with different interfaces will simultaneously be executing.
There are a collection of techniques used to overcome these inconsistencies—enforcing backward compatibility, using feature toggles, gracefully handling unknown responses from a dependent service—but the first step is recognizing that deployment and deployment strategies can cause inconsistencies when multiple instances of a service are being run.
18.118.24.228