Quality attribute

Quality attributes are the measurable properties of a software system that describe how well it behaves, as distinct from what it does. They manifest as the cross-cutting concerns that affect the overall operation of a system, and which are the primary focus of software architecture.

Quality attributes are specified as non-functional requirements. They include the following.

Many quality attributes carry names ending in -ility, which is why the collection is sometimes called the ilities.

A useful grouping separates attributes observable while a system is running (performance, availability, security, usability) from those observable only as the system changes over time (modifiability, testability, portability, evolvability).

Trade-offs between attributes

Quality attributes rarely improve in isolation. Most are in tension with at least one other attribute. The work of software architecture involves resolving those tensions.

Everything in software architecture is a trade-off.

– Mark Richards and Neal Ford
Fundamentals of Software Architecture (O'Reilly)

The CAP theorem and its generalization the PACELC theorem state one such trade-off, that in replicated data stores, consistency, availability, and latency cannot all be maximized at once. An architecture must therefore declare which of these competing qualities it sacrifices.

Software architects resolve these tensions with architectural tactics. These are catalogued design decisions known to move a particular attribute in a particular direction, such as caching for latency, replication for availability, or rate limiting for stability under load. Each tactic trades one attribute for another, so choosing among them is the trade-off. Standards such as the ISO/IEC 25010 SQuaRE family name the attributes a tactic might target, giving the conversation a shared vocabulary.

Classic trade-offs in system design include:

  • Monolithic versus distributed architecture. \ Monolithic systems are simpler to develop and deploy, but they can be harder to maintain and are limited to vertical scalability. Distributed software is more complex to develop and deploy, but individual parts are easier to maintain and there is greater scope for scalability using horizontal scaling techniques. Modular monoliths provide a middle ground and allow for the incremental transition of a monolith to distributed software.
  • Vertical versus horizontal scaling. \ Scaling vertically is simpler but there’s a physical limit to how much you can upgrade individual nodes, and it maintains single points of failure. Scaling horizontally is more complicated and it adds whole new categories of accidental complexity, such as those related to achieving consistency, but it allows for almost limitless scaling and, as a happy byproduct, has benefits for fault tolerance.
  • Strong versus eventual consistency. \ Strong consistency ensures that all read operations return the most recent write for a given piece of data, but guaranteeing this typically requires synchronous coordination between nodes, which costs latency and, per the CAP theorem, availability during a network partition. Eventual consistency ensures that, given enough time, all nodes in a distributed program will converge to the same value, trading the occasional stale reads for lower latency and higher availability.
  • Stateful versus stateless design. \ In a stateful design, the system remembers client data from one request to the next. The server maintains a record of the client’s state, which may include session information, transaction details, or any other data relevant to the ongoing interaction, at the cost of tying a client to a specific node and complicating horizontal scaling. Stateless design treats each request as an independent transaction, buying scalability and fault-tolerance, but each request must then carry all the information necessary for the server to fulfill it, which costs latency and bandwidth, and prevents the server from building on prior interactions.
  • Read-through versus write-through caching. \ A read-through cache loads and caches data on a miss, giving low read latency at the risk of serving stale data. A write-through cache writes to the cache and the data store together on every write, keeping the two in sync at the cost of higher write latency.
  • Relational (SQL) versus non-relational (NoSQL) databases. \ Relational databases give you ACID transactions and enforced data integrity, but a schema that is costly to change once data is flowing. NoSQL databases trade away that integrity and, often, strong consistency in exchange for schema flexibility and easier horizontal scaling.
  • Normalization versus denormalization. \ Normalization reduces redundancy and protects data integrity, at the cost of the joins needed to reassemble related data at read time. Denormalization trades that write-time simplicity and storage economy for faster reads, by duplicating data across tables.
  • Synchronous versus asynchronous communication. \ Synchronous communication is easier to reason about, but it couples the caller to the callee’s availability. A slow or failing callee holds the caller’s thread open and can cascade into a wider failure. Asynchronous communication decouples the two and improves fault tolerance, at the cost of the added complexity of managing work that completes out of band.
  • Batch versus stream processing. \ Batch processing achieves high throughput by processing large volumes of data together, but results are unavailable until the whole batch completes. Stream processing trades some of that throughput efficiency for low latency, processing each event as it arrives.
  • TCP versus UDP. \ TCP guarantees ordered, error-free delivery via connection setup, acknowledgements, and retransmission, at the cost of the latency and overhead that reliability requires. UDP drops those guarantees for minimal overhead and lower latency, leaving error checking and ordering to the application, if it needs them at all.
  • REST versus RPC-style APIs. \ REST’s resource-oriented, standard-HTTP-verb style makes APIs predictable and cacheable, which suits public-facing APIs, but its CRUD-shaped resources are a poor fit for arbitrary actions and can lead to over-fetching. RPC-style APIs model actions directly and can be more efficient, which suits internal service-to-service calls, but at the cost of tighter coupling between client and server.
  • Push versus pull-based communication. \ Push lets the server notify the client the moment new data exists, avoiding the latency and overhead of polling, but requires the server to track and manage client connections or delivery, eg. via webhooks. Pull keeps the server simple and puts the client in control of when it syncs, at the cost of the client only ever learning of changes when it next asks.
  • Bidirectional versus unidirectional protocols. \ Bidirectional protocols like WebSockets allow the server to push data over a persistent connection, but that connection makes the server stateful, which complicates horizontal scaling in a way that stateless HTTP does not need to worry about. Unidirectional protocols require the client to request each piece of data, which is less efficient for real-time use cases. Long-polling mimics bidirectional delivery over a unidirectional protocol, at the cost of the server having to hold many concurrent open requests.

Guarding attributes

Once specified, quality attributes can be guarded by fitness functions – automated checks that fail when the system drifts away from the attribute’s target value. The broader discipline of quality assurance provides the processes and evidence that a system in fact meets its specified attributes.

See also

References

  • Bass, Clements, and Kazman (2021). Software Architecture in Practice (4th ed.). Addison-Wesley.
  • ISO/IEC (2023). ISO/IEC 25010:2023 — Systems and software Quality Requirements and Evaluation (SQuaRE) — Quality model.
  • Richards and Ford (2020). Fundamentals of Software Architecture. O’Reilly.