Introduction
Every architecture diagram tells you what a system does. Almost none of them tell you how the system behaves when a dependency times out at 2 a.m., when traffic spikes ten times overnight, or when a security patch has to ship without downtime. That behavior is governed by a set of properties architects usually call "architectural characteristics" - and older texts call "non-functional requirements" or "-ilities." They are not features. Nobody files a ticket asking for "more scalability." Yet they determine, more than any single design pattern, whether a system is pleasant to operate or a permanent source of on-call dread.
This article works through twelve of the most consequential characteristics - availability, scalability, fault tolerance, recoverability, deployability, reliability, security, elasticity, performance, learnability, testability, and agility - with an emphasis on how they interact, where they conflict, and how experienced teams actually reason about them. The goal isn't a glossary. It's a working mental model you can bring into a design review, an incident postmortem, or a conversation with a product manager who wants to know why "just add a cache" isn't a five-minute change.
Why Architectural Characteristics Deserve Their Own Conversation
Most engineering conversations gravitate toward functional requirements because they're concrete: a login form, a checkout flow, a search endpoint. Architectural characteristics are harder to talk about because they're cross-cutting - they don't live in a single module, they emerge from how modules are connected, deployed, and operated. Mark Richards and Neal Ford, in Fundamentals of Software Architecture (O'Reilly), describe this as the difference between structural and operational concerns, and argue that an architecture is defined as much by its "-ilities" as by its components and connectors.
The practical consequence is that these characteristics are frequently invisible until they're violated. A system can pass every functional test and still fail in production because nobody decided, explicitly, how many nines of availability it needed, or what "recoverable" meant for that particular dataset. ISO/IEC 25010, the international standard for software product quality, formalizes this by defining quality characteristics such as reliability, performance efficiency, security, maintainability, and portability as first-class dimensions of software quality, separate from functional suitability. Treating them as an afterthought is one of the most reliable ways to accumulate technical debt that no amount of refactoring will fix, because the debt is architectural, not just structural.
There's also a prioritization problem. You cannot maximize all twelve characteristics simultaneously - they trade off against each other constantly. Maximizing security often reduces performance. Maximizing elasticity often complicates testability. A mature architecture practice doesn't chase an impossible ideal of "excellent at everything"; it makes an explicit, negotiated decision about which three or four characteristics matter most for a given system, and designs deliberately around that priority list. That negotiation, more than any diagram, is the real deliverable of an architecture phase.
The Twelve Characteristics, Explained
Before getting into implementation details, it helps to define each characteristic precisely, since these terms get used loosely in casual conversation. The definitions below draw on common industry usage as reflected in ISO/IEC 25010, the AWS Well-Architected Framework, and Google's Site Reliability Engineering book, all of which converge on broadly similar meanings even when their taxonomies differ slightly.
Availability
Availability measures the percentage of time a system is capable of servicing requests, typically expressed in "nines" (99.9%, 99.99%, and so on). It's a function of both how often a system fails and how quickly it recovers, which is why availability calculations combine mean time between failures (MTBF) and mean time to recovery (MTTR). A system that fails often but recovers instantly can have the same availability number as one that rarely fails but takes hours to restart - the number alone doesn't tell you which failure mode you're dealing with.
Scalability
Scalability is the ability of a system to handle increased load - more users, more data, more transactions - without a proportional degradation in performance. It's usually discussed in two forms: vertical scaling (bigger machines) and horizontal scaling (more machines), with horizontal scaling generally preferred in cloud-native architectures because it avoids hard ceilings on hardware capacity.
Fault Tolerance
Fault tolerance is a system's ability to continue operating, possibly in a degraded mode, when a component fails. This is distinct from availability: a fault-tolerant system anticipates specific failure modes (a downstream service timing out, a node crashing) and has explicit mechanisms - retries, fallbacks, circuit breakers - to contain them rather than letting them cascade.
Recoverability
Recoverability is how quickly and completely a system can restore normal operation after a disruption, whether that's a failed deployment, a corrupted database, or a full region outage. It's typically measured with two targets: Recovery Time Objective (RTO), how long recovery should take, and Recovery Point Objective (RPO), how much data loss is acceptable.
Deployability
Deployability describes how easily and safely new code can be shipped to production. High deployability means small, frequent, low-risk releases; low deployability means big-bang deployments that require change-freeze windows and war rooms. This characteristic is central to the practices described in Jez Humble and David Farley's Continuous Delivery and is one of the four key metrics tracked in the DORA (DevOps Research and Assessment) research program.
Reliability
Reliability is the probability that a system performs its intended function correctly over a given period, under stated conditions. It overlaps with availability but is narrower: a system can be "up" (available) while returning incorrect results, which would make it unreliable despite being technically available.
Security
Security covers confidentiality, integrity, and availability of data and functionality against unauthorized access or misuse (the classic "CIA triad"). Architecturally, it spans authentication, authorization, encryption in transit and at rest, network segmentation, and secure defaults, and it's increasingly treated as a layered concern rather than a single perimeter, an approach often summarized as "defense in depth," a concept documented extensively by NIST.
Elasticity
Elasticity is closely related to scalability but specifically refers to a system's ability to scale resources down as well as up, automatically, in response to demand. Where scalability answers "can this grow," elasticity answers "does this shrink back and stop costing money when demand drops." Cloud auto-scaling groups and Kubernetes Horizontal Pod Autoscalers are canonical implementations.
Performance
Performance covers response time, throughput, and resource efficiency under expected load. It's usually expressed with percentile latencies (p50, p95, p99) rather than averages, because averages hide the tail latencies that actually determine user-perceived slowness - a point made repeatedly in Google's SRE literature and in performance engineering practice generally.
Learnability
Learnability is how quickly a new engineer can understand a system well enough to make a safe change. It's rarely discussed explicitly, but it directly affects onboarding time, bus-factor risk, and the sustainability of a codebase as team composition changes over years.
Testability
Testability is how easily a system's correctness can be verified through automated tests. It's strongly influenced by architectural decisions - tight coupling, hidden global state, and unmockable external dependencies all reduce testability regardless of how disciplined individual engineers are about writing tests.
Agility
Agility, in the architectural sense, is how quickly a system can absorb new requirements without a disproportionate increase in cost or risk. It's the composite outcome of good modularity, low coupling, and high deployability, and it's often the characteristic that determines whether a system is still viable five years after launch or has become something teams route around.
Implementation Patterns in Practice
Abstract definitions only go so far; these characteristics are usually implemented through a fairly small set of recurring patterns. Fault tolerance, for instance, is commonly implemented with the circuit breaker pattern, which stops calling a failing dependency after a threshold of errors, giving it time to recover instead of hammering it with retries. The example below shows a minimal but realistic circuit breaker in TypeScript, the kind of building block that libraries like Polly (.NET) or resilience4j (Java) implement more fully, but which is worth understanding at the mechanism level.
type CircuitState = "CLOSED" | "OPEN" | "HALF_OPEN";
class CircuitBreaker {
private state: CircuitState = "CLOSED";
private failureCount = 0;
private lastFailureTime = 0;
constructor(
private readonly failureThreshold: number = 5,
private readonly resetTimeoutMs: number = 30_000
) {}
async execute<T>(operation: () => Promise<T>): Promise<T> {
if (this.state === "OPEN") {
const elapsed = Date.now() - this.lastFailureTime;
if (elapsed < this.resetTimeoutMs) {
throw new Error("Circuit is OPEN: refusing call to protect downstream");
}
this.state = "HALF_OPEN";
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (err) {
this.onFailure();
throw err;
}
}
private onSuccess(): void {
this.failureCount = 0;
this.state = "CLOSED";
}
private onFailure(): void {
this.failureCount += 1;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = "OPEN";
}
}
}
Recoverability and reliability are usually addressed at a different layer: health checks and readiness probes that let an orchestrator decide whether an instance should receive traffic at all. The Python example below models the kind of readiness endpoint commonly deployed behind Kubernetes or a load balancer, distinguishing between "the process is running" (liveness) and "the process is ready to serve" (readiness), a distinction that directly affects both availability and fault tolerance during rolling deployments.
from dataclasses import dataclass
from enum import Enum
import time
class DependencyStatus(Enum):
HEALTHY = "healthy"
DEGRADED = "degraded"
DOWN = "down"
@dataclass
class HealthCheckResult:
status: DependencyStatus
latency_ms: float
checked_at: float
class ReadinessProbe:
def __init__(self, checks: dict):
# checks: name -> callable returning bool
self.checks = checks
self.cache: dict[str, HealthCheckResult] = {}
self.cache_ttl_seconds = 5
def check(self, name: str) -> HealthCheckResult:
cached = self.cache.get(name)
if cached and (time.time() - cached.checked_at) < self.cache_ttl_seconds:
return cached
start = time.time()
try:
ok = self.checks[name]()
status = DependencyStatus.HEALTHY if ok else DependencyStatus.DEGRADED
except Exception:
status = DependencyStatus.DOWN
latency = (time.time() - start) * 1000
result = HealthCheckResult(status, latency, time.time())
self.cache[name] = result
return result
def is_ready(self) -> bool:
# A service is "ready" only if no dependency is fully DOWN.
# DEGRADED dependencies may still allow traffic (graceful degradation).
return all(
self.check(name).status != DependencyStatus.DOWN
for name in self.checks
)
Deployability, meanwhile, is less about a single code pattern and more about pipeline design: feature flags to decouple deploy from release, blue-green or canary deployment strategies to limit blast radius, and automated rollback triggers tied to error-rate metrics. These practices are what allow deployability and reliability to coexist rather than trade off against each other - a well-designed pipeline makes frequent deploys safer, not riskier, which is the central finding of the DORA State of DevOps research linking deployment frequency to organizational performance rather than treating it as purely a speed metric.
Trade-offs and Common Pitfalls
The hardest part of working with architectural characteristics isn't understanding any one of them individually - it's recognizing that they pull against each other, often invisibly, until a design review or an incident forces the tension into the open. Security and performance are a classic pair: encrypting every internal service call, validating every input against a strict schema, and enforcing fine-grained authorization checks all add latency. None of this argues against security controls; it argues for measuring their cost explicitly rather than assuming they're free, and for placing expensive checks (like full audit logging) at boundaries where they matter most rather than uniformly everywhere.
Scalability and testability conflict in a subtler way. Systems designed for horizontal scale often rely on eventual consistency, asynchronous messaging, and distributed state, all of which make deterministic testing harder. A test suite that worked cleanly against a single-node monolith can become flaky and slow once the same logic is spread across services communicating over a message broker, because tests now have to account for ordering, retries, and partial failures that didn't exist before. Teams that scale successfully usually invest deliberately in contract testing and consumer-driven contracts (a pattern popularized by tools like Pact) specifically to keep testability from silently degrading as the system becomes more distributed.
A less discussed but equally real conflict is between agility and reliability. Teams under pressure to ship features quickly sometimes treat reliability work - retry logic, chaos testing, capacity planning - as work that can be deferred indefinitely because it doesn't map to a visible user-facing feature. This is a false economy: reliability debt compounds the same way code debt does, and it tends to come due at the worst possible moment, during a traffic spike or a major launch, when the cost of fixing it is highest and the political cost of the outage is most visible.
Perhaps the most common pitfall of all is treating elasticity as a substitute for capacity planning. Auto-scaling handles gradual or moderately fast demand changes well, but it has real limits: instances take time to boot, connection pools take time to warm, and downstream databases don't auto-scale as gracefully as stateless application tiers. Teams that rely entirely on elasticity without load testing their scaling policies often discover, during an actual spike, that the system scales too slowly to prevent a period of degraded availability before capacity catches up - which is really an availability failure wearing an elasticity costume.
Best Practices for Working with Architectural Characteristics
The single highest-leverage practice is making trade-offs explicit rather than implicit. Before a system is built, the team should agree - in writing, ideally as an architecture decision record (ADR) - on which three or four characteristics are the top priority for that specific system, and which are explicitly secondary. An internal reporting tool used by twelve people has fundamentally different priorities than a payment processing API, even though both might technically need "availability" and "security." Naming the priorities up front turns later trade-off decisions from arguments into applications of an already-agreed policy.
Second, measure characteristics, don't just declare them. "Highly available" and "fast" are not engineering statements; "99.95% availability measured monthly" and "p99 latency under 300ms at 5x current peak load" are. Service Level Objectives (SLOs), as described in Google's SRE practice, exist precisely to convert vague aspirations into numbers that can be tested, alerted on, and used to make real prioritization decisions - including the decision to deliberately slow down feature work when an error budget is exhausted.
Third, test the characteristics you claim to have, not just the functionality you've built. Chaos engineering - deliberately injecting failures into a system to verify it behaves as expected, an approach pioneered by Netflix's Chaos Monkey - exists because fault tolerance and recoverability are claims that are false until verified under real failure conditions. Load testing serves the same role for performance and scalability claims, and security testing (including regular penetration testing and dependency scanning) serves it for security claims.
Fourth, revisit priorities as the system and its context change. A system's ideal characteristic priorities at launch, when correctness and learnability matter most because the team is still small and iterating quickly, are rarely the same as its priorities two years later at scale, when availability and performance under load dominate. Architecture reviews should treat the priority list itself as a living artifact, not a decision made once and never revisited.
Key Takeaways
- Rank, don't maximize. Pick the three or four characteristics that matter most for your specific system and be explicit about which ones you're consciously deprioritizing.
- Turn adjectives into numbers. Replace "fast" and "reliable" with measurable SLOs (e.g., p99 latency, monthly availability percentage) that can actually be tested and alerted on.
- Verify, don't assume. Use chaos engineering, load testing, and security scanning to confirm that claimed characteristics actually hold under real failure and load conditions.
- Design deployability in from the start. Feature flags, canary releases, and automated rollback are what let you ship frequently and reliably, rather than choosing between the two.
- Revisit the priority list periodically. The right trade-offs at launch are rarely the right trade-offs at scale; treat architectural priorities as something to reassess, not a decision made once.
Conclusion
Architectural characteristics are easy to nod along to in the abstract and easy to get wrong in practice, because the cost of ignoring any single one of them rarely shows up immediately. A system with no explicit fault tolerance strategy works fine - until a downstream dependency has a bad day. A system with no deployability investment ships fine - until the tenth "quick hotfix" requires a four-hour maintenance window. The twelve characteristics covered here aren't a checklist to complete once; they're a lens for evaluating every significant design decision, from choosing a database to structuring a deployment pipeline.
The teams that handle this well don't have a secret technique for satisfying all twelve simultaneously - nobody does, because the trade-offs are real and unavoidable. What they have is a habit of naming the trade-off out loud, writing it down, and measuring whether the resulting system actually behaves the way they intended. That habit, more than any specific pattern or tool mentioned in this article, is what separates architecture as a deliberate practice from architecture as something that happens to a codebase by accident.
References
- Richards, Mark, and Neal Ford. Fundamentals of Software Architecture: An Engineering Approach. O'Reilly Media, 2020.
- ISO/IEC 25010:2011, Systems and software engineering - Systems and software Quality Requirements and Evaluation (SQuaRE) - System and software quality models. International Organization for Standardization.
- Humble, Jez, and David Farley. Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation. Addison-Wesley, 2010.
- Beyer, Betsy, Chris Jones, Jennifer Petoff, and Niall Richard Murphy, eds. Site Reliability Engineering: How Google Runs Production Systems. O'Reilly Media, 2016. Available at https://sre.google/sre-book/table-of-contents/
- Google Cloud, DORA (DevOps Research and Assessment) - State of DevOps Reports. https://dora.dev/
- Amazon Web Services, AWS Well-Architected Framework. https://aws.amazon.com/architecture/well-architected/
- National Institute of Standards and Technology (NIST), NIST Special Publication 800-53: Security and Privacy Controls for Information Systems and Organizations. https://csrc.nist.gov/publications/detail/sp/800-53/rev-5/final
- Netflix Technology Blog, The Netflix Simian Army (Chaos Monkey / chaos engineering). https://netflixtechblog.com/the-netflix-simian-army-16e57fbab116
- Pact Foundation, Contract Testing Documentation. https://docs.pact.io/
- Kubernetes Documentation, Configure Liveness, Readiness and Startup Probes. https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/