At the first level of decomposition, a software requirement can be classified as either “functional” or “non-functional“. A functional requirement (FR) specifies a value-added behavior/capability/property that the software system must exhibit during runtime in order for it to be deemed acceptable by its customers and users. A non-functional requirement (NFR) specifies how well one or more behaviors/capabilities/properties must perform.
Unlike FRs, which are nominally local, fine-grained, and verifiable early in the project, NFRs are typically global, coarse-grained, and unverifiable until a considerable portion of the system has been developed. Also, unlike FRs, NFRs often have cold, hard-to-meet, numbers associated with them:
- The system shall provide a response to any user request under a full peak load of 1000 requests per second within 100 milliseconds. (throughput, latency NFRs)
- The system shall be available for use 99.9999 percent of the time. (availability NFR)
- The system shall transition from the not-ready to ready within 5 seconds. (Domain-specific Performance NFR)
- The system shall be repairable by a trained technician within 10 minutes of the detection and localization of a critical fault. (Maintainability NFR)
- The system shall be capable of simultaneously tracking 1000 targets in a clutter-dominated environment. (Domain-specific Performance NFR)
- The system shall detect an air-breathing target with radar cross-section of 1 sq. meter at a range of 200 miles with a probability of 90 percent. (Domain-specific Performance NFR)
- The system shall produce range estimates within 5 meters of the true target position. (Domain-specific Performance NFR)
- The system shall provide TCP/IP, HTTP, CORBA, and DDS protocol support for interfacing with external systems. (Interoperability NFR)
During front-end system conception, NFRs are notoriously underspecified – if they’re formally specified at all. And yet, unlike many functional requirements, the failure to satisfy a single NFR can torpedo the whole effort and cause much downstream, career-ending, mischief after a considerable investment has been sunk into the development effort.
Since extra cross-cutting functionality (and extra hardware) must often be integrated into a system architecture (from the start) to satisfy one or more explicit/implicit NFRs, the term “non-functional requirement” is often a misnomer. Thus, as the SEI folks have stated, “Quality Attribute Requirement” (QAR) is a much more fitting term to specify the “how-well-a-system-must-perform” traits:
As the figure below illustrates, the “QARization” of a system can substantially increase its design and development costs. In addition, since the value added from weaving in any extra QAR software and hardware is mostly hidden under the covers to users and only becomes visible when the system gets stressed during operation, QARs are often (purposely?) neglected by financial sponsors, customers, users, AND developers. Stakeholders typically want the benefits without having to formally specify the details or having to pay the development and testing costs.
It’s sad, but like Rodney Dangerfield, QARs get no respect.
Charlie Alfred is a highly intelligent and eloquent writer on the topic of software architecture. He is the first (and still, only) internet friend that I’ve met in real life; face-to-face. We hooked up for lunch to shoot the shit twice when he was visiting a client of his located in my neighborhood.
Charlie is the second person to affirm my deeply held belief that “context is king” in software architecture. He also taught me about the concept of “the architecture of the problem“. But like all good relationships, ours isn’t all about mushy love, kisses, and total agreement. Charlie often provides valuable counter-intelligence to my blustery blogging BS that causes me to step back, reconsider, and often soften my beliefs.
Since my friend Charlie hasn’t been feeling well lately and I haven’t interacted with him in a while, I’d like to wish Charlie and his family a very special, very merry, Christmas.
A heartfelt thanks to Ruth Malan for suggesting the idea of this post.
There are two common perspectives on the process of architectural design, whether it be for buildings or for software. The first is that a designer starts with nothing—a blank slate, whiteboard, or drawing board—and builds-up an architecture from familiar components until it satisfies the needs of the intended system. The second is that a designer starts with the system needs as a whole, without constraints, and then incrementally identifies and applies constraints to elements of the system in order to differentiate the design space and allow the forces that influence system behavior to flow naturally, in harmony with the system. Where the first emphasizes creativity and unbounded vision, the second emphasizes restraint and understanding of the system context. – “RESTful” Roy Fielding
It might not be a correct interpretation, but BD00 associates Mr. Fielding’s two alternatives with “inside-out” and “outside-in” design.
The figure below illustrates the process of inside-out design. The designer iteratively composes a structure and “hopes” it will integrate smoothly downstream into the context for which it is intended. During the inside-out design process, the parts are king and the system context is secondary.
The figure below depicts an outside-in design process. The designer iteratively composes a structure within the bounded constraints of the context (the “whole“) for which it is intended. During the outside-in design process, system context is king and the parts are secondary.
Because system contexts can vary widely from system to system and they’re usually vaguely defined, messy, and underspecified, designers often opt for the faster inside-out approach. BD00 uses the outside-in design process. What process do you use?
When creating an architecture or detailed design of a system component, up to three forms of design evaluation can be performed:
- A: Continuous evaluation by the designer during the process
- B: Evaluation by a peer group at one or more review events during the process
- C: Evaluation by an expert outsider group after A and B
These three forms of evaluation are illustrated in the sequence of diagrams below. Note that as each evaluation type is progressively added to the mix, a new feedback loop is introduced into the system.
During A, the conceive/evaluate/repair loop is performed at the speed of thought within the isolated mind of the designer. As the designer applies his skills to his understanding of the problem to be solved, alternative component structures and configurations can pop up instantaneously during each of many high frequency loop traversals. At some point, the designer concludes that his design solution is stable enough for external scrutiny and/or ready for the next step of realization.
During B, the designer walks through his creation in front of a group of peers. He explains the structure and behavior mechanisms of his design and how it solves the problem. If the peer group is qualified, prepared, and objective (QPO), at least a portion of the feedback the group produces will be valuable to the designer and he will use it to improve his design. However, unless all three QPO pre-conditions are satisfied, the peer review process will be a huge waste of time and money.
Jetting in an expert outsider group is the riskiest type of evaluation. Assuming that the QPO criteria is satisfied, the fact that the expert outsider group has no real “skin in the game” after it vacates the premises should be a cause for concern to anyone contemplating the use of the technique.
In general, the more negative feedback loops incorporated into a process, the more likely the process is to produce its intended output. As the final figure above implies, incorporating all three types of evaluation into a design review process can lead to a high quality design. However, blindly ignoring the QPO criteria and/or failing to address the “skin in the game” risk can increase cost, lengthen schedule, and lay waste to your well-intentioned review process – without you ever knowing that it occurred. As ever, the devil is in the details.
Assume that your team was tasked with developing a large software system in any application domain of your choice. Also, assume that in order to manage the functional complexity of the system, your team iteratively applied the “separation of concerns” heuristic during the design process and settled on a cleanly layered system as such:
So, how are you gonna manifest your elegant paper design as a working system running on real, tangible hardware? Should you build it from the bottom up like you make a cake, one layer at a time?
Or, should you build it like you eat a cake, one slice at a time?
The problem with growing the system layer-by-layer is that you can end up developing functionality in a lower layer that may not ever be needed in the higher layers (an error of commission). You may also miss coding up some lower layer functionality that is indeed required by higher layers because you didn’t know it was needed during the upfront design phase (an error of omission). By employing the incremental slice-by-slice method, you’ll mitigate these commission/omission errors and you’ll have a partially working system at the end of each development step – instead of waiting until layers 1 and 2 are solid enough to start adding layer 3 domain functionality into the mix.
In the context of organizational growth, Russell Ackoff once stated something like: “it is better to grow horizontally than vertically“. Applying Russ’s wisdom to the growth of a large software system:
It’s better to grow a software system horizontally, one slice at a time, than vertically, one layer at a time.
The above quote is not some profound, original, BD00 quote. It’s been stated over and over again by multitudes of smart people over the years. BD00 just put his own spin on it.
On the left we have the process of abstract decomposition, and on the right we have the process of concrete composition:
Note that during the concrete composition from parts to final system on the right, we gracefully transition through two stable, intermediate forms. Some people and communities, especially those obsessed with “velocity” and “time-to-market“, would say “bollocks” to all those value-subtracting, intermediate steps. We no need no stinking intermediate forms:
The following C++14 code fragment represents a general message layout along with a specific instantiation of that message:
Side note: Why won’t a C++98/03 compiler accept the above code?
Assume that we are “required” to send thousands of these X-Y position messages per second between two computers over a finite bandwidth communication link:
There are many ways we can convert the representation of the message in memory into a serial stream of bytes for transmittal over the communication link, but let’s compare a simple binary representation against an XML equivalent:
The tradeoff is simple: human readability for performance. Even though the XML version is self-describing and readable to a human being, it is 6.5 times larger than the tight, fixed-size, binary format. In addition, the source code required to serialize/deserialize (i.e. marshal/unmarshal) the XML version is computationally denser than the code to implement the same functionality for the fixed-size, binary representation. In the software industry, this tradeoff is affectionately known as “the angle bracket tax” that must be payed for using XML in the critical paths of your system.
If your system requires high rates of throughput and low end-to-end latency for streaming data over a network, you may have no choice but to use a binary format to send/receive messages. After all, what good is it to have human readable messages if the system doesn’t work due to overflowing queues and lost messages?