Functional vs Non-functional Requirements (Why "Fast and Reliable" Is Not an Architectural Requirement) | PulseSoft
PulseSoft

Functional vs Non-functional Requirements (Why "Fast and Reliable" Is Not an Architectural Requirement)

PulseSoft Team · October 8, 2026 · 14 min read

Two teams can build a checkout service from the same product spec, ship the same endpoints, and pass the same feature tests. One survives a payment provider slowdown with a modest rise in latency. The other exhausts its connection pools, triggers a retry storm, and double-charges a fraction of its customers. Neither team misread the spec. The spec simply never said what the system had to guarantee.

That gap is the real subject of functional vs non-functional requirements. Functional requirements describe what a user can do: place an order, cancel a subscription, search a catalog. Non-functional requirements describe the properties the system must hold while doing it: how available, how fast, how durable, how consistent, under what load. The first category shapes your API. The second shapes your architecture.

The difficulty is that non-functional requirements usually arrive as adjectives ("fast," "reliable," "scalable") rather than numbers, and adjectives cannot be designed against or tested. This article covers how to separate the two categories precisely, how to turn quality attributes into measurable targets, how to find the constraints nobody wrote down, and why each number you commit to has a direct, often expensive, architectural consequence.

The Core Problem: Capabilities Are Easy to State, Guarantees Are Not

A functional requirement is a statement about behavior under assumed-normal conditions. "A customer can place an order" implicitly assumes the database answers, the payment provider responds, the network delivers the request once, and the user clicks the button once. Functional tests typically run in exactly that world.

A non-functional requirement is a statement about behavior when those assumptions weaken. It answers questions the functional spec skips: What happens when the database takes 800 ms instead of 8 ms? What happens when the request is delivered twice? What happens at three times expected traffic? How much data can be lost if a node dies mid-write?

These assumptions exist because they are true most of the time. They stop being true at precisely the moments that matter, and in a distributed system they stop being true independently. The API process can be healthy while its database is degraded, its cache is cold, and one of three replicas is lagging.

Slow is worse than down

Consider a concrete system. A request enters an API service, which writes to a database and publishes an event to a message broker. The functional spec says "orders are saved and downstream systems are notified." Now suppose the database does not fail. It just becomes slow.

The causal chain looks like this:

DB latency rises from 10 ms to 2 s
  -> each request holds a DB connection 200x longer
  -> connection pool exhausts
  -> requests queue waiting for a connection
  -> end-to-end latency exceeds client timeouts
  -> clients retry
  -> offered load rises while capacity has already fallen
  -> more queueing, more timeouts, more retries

Little's Law makes the math explicit. The number of requests in flight equals arrival rate multiplied by time in system (L = λW). At 500 requests per second and 20 ms per request, about 10 requests are in flight. At the same arrival rate with 2 seconds per request, about 1,000 are in flight. A pool of 50 connections that was comfortably oversized is now short by a factor of twenty.

A complete outage produces fast errors that clients can handle. A slow dependency consumes threads, sockets, and memory everywhere upstream. Nothing in the functional requirements distinguishes these cases, but they demand entirely different designs. The non-functional requirements are where that distinction must live: a latency budget, a timeout policy, a maximum queue depth, and a defined behavior when the budget is exceeded.

Turning Quality Attributes Into Testable Requirements

Non-functional requirements are often called software architecture quality attributes, and the useful ones share a shape: a metric, a target, a measurement window, and a condition. "The system should be fast" has none of these. "p99 latency for POST /orders is under 300 ms, measured at the load balancer over rolling 5-minute windows, at up to 2,000 requests per second" has all four. Only the second can fail a test, which is exactly why it is useful.

Availability is a budget, not an aspiration

Availability targets translate directly into permitted downtime:

TargetDowntime per 30-day monthDowntime per year
99%~7.2 hours~3.65 days
99.9%~43 minutes~8.8 hours
99.95%~22 minutes~4.4 hours
99.99%~4.3 minutes~53 minutes

The jump from three nines to four is not a 0.09% improvement. It is a 10x reduction in tolerable failure, and it changes what the operating model must be. At 43 minutes a month, a human can notice, diagnose, and roll back. At 4 minutes a month, detection and recovery must largely be automated, deployments must be progressive, and a single regional dependency is probably disqualifying.

Availability also composes multiplicatively for serial dependencies. A request path that synchronously touches five components, each at 99.9%, has a ceiling of roughly 0.999^5 ≈ 99.5%. This is the most common way a stated requirement silently becomes impossible: the target is set for the service, but the service sits on a chain of dependencies whose combined availability is lower. The architectural response is either to remove dependencies from the critical path (make them asynchronous, cache them, or degrade gracefully without them) or to lower the target.

It also matters how availability is measured. "Uptime of the process" and "fraction of user requests that succeeded" are different numbers. A request-based definition, an SLI in SRE terminology, is almost always the one that reflects what users experience.

Latency must be stated as a distribution

Averages hide the behavior that hurts. A service with a 40 ms mean can have a p99 of 2 seconds, meaning one in a hundred requests is slow, and a user session that makes dozens of requests will very likely encounter one.

Fan-out amplifies this sharply. If a page requires responses from 100 backend calls, and each call independently has a 1% chance of exceeding one second, the probability that at least one is slow is 1 − 0.99^100, roughly 63%. The tail of each component becomes the median experience of the aggregate. This is why latency requirements should specify percentiles (p50, p99, sometimes p99.9) and why high fan-out systems often need techniques such as hedged requests or partial responses that a single-call system never needs.

A latency requirement also implies a budget that must be divided across the call path. If POST /orders must complete within 300 ms at p99, and payment authorization alone can take 200 ms at its own p99, then the remaining work has 100 ms. That arithmetic constrains timeouts, retry counts, and whether certain steps can stay synchronous at all.

// The latency requirement becomes a deadline that propagates through the call path.
// Each downstream call receives only the time that remains, so a slow dependency
// cannot consume more than the request was ever allowed to spend.
func (h *OrderHandler) Create(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithTimeout(r.Context(), 300*time.Millisecond)
	defer cancel()

	if err := h.inventory.Reserve(ctx, req); err != nil {
		writeError(w, err) // includes context.DeadlineExceeded
		return
	}
	// Payment gets whatever budget is left, never a fresh 30s default.
	if err := h.payments.Authorize(ctx, req); err != nil {
		writeError(w, err)
		return
	}
}

The point of the snippet is not the syntax. It is that a non-functional requirement only becomes real when it is encoded in mechanisms (deadlines, pool sizes, queue limits) rather than in documents.

Durability, consistency, and delivery are guarantees with prices

Some of the most consequential non-functional requirements hide inside functional language.

"Orders must never be lost" is a durability requirement. It means a success response may only be returned after the order is persisted somewhere that survives the failure of the node that accepted it. If "never" means surviving the loss of an availability zone, writes must be synchronously replicated across zones before acknowledgment, which adds a network round trip to every write. In disaster-recovery terms this is a recovery point objective (RPO) of zero, and RPO zero is never free.

"The customer should see their order right after placing it" sounds like it demands strong consistency. Usually it demands something narrower: read-your-writes consistency for that user's session. That can be satisfied by routing the user's reads to the primary for a short window or by returning the created resource in the response, without forcing every reader in the system onto the primary.

"Charge the customer exactly once" cannot be satisfied by the network. Over an unreliable network, a client that times out cannot know whether the server processed the request, so it must either retry (risking duplicates) or give up (risking loss). Exactly-once delivery is not achievable in that model; exactly-once effect is, through idempotency keys and deduplication at the point where the side effect happens. The requirement must therefore be restated as an invariant ("at most one successful charge per order ID") and assigned to a specific component that enforces it.

Enjoying this so far?

Join thousands of engineers receiving practical engineering, Cloud, Distributed Systems, Infrastructure and AI engineering insights.

No spam. Unsubscribe anytime.

Exposing Hidden Constraints

The requirements that cause the most architectural damage are the ones nobody states because everyone assumes them. A useful practice is to interrogate every functional requirement with a fixed set of questions:

  • Load: What is the expected peak rate, and what is the shape of the peak? A flash sale with 50x traffic in 60 seconds is a different system from steady 50x growth over a year.
  • Data growth: What happens at 10x and 100x data volume? Which queries become scans? Which indexes stop fitting in memory?
  • Failure tolerance: Which dependencies can be down while this capability still works, perhaps in degraded form?
  • Freshness: How stale can each piece of data be? Product descriptions can tolerate minutes; inventory counts during a sale may tolerate seconds; account balances may tolerate none.
  • Invariants: What must never be true? Overselling inventory, double-charging, or exposing one tenant's data to another are invariants, and they deserve explicit owners.
  • External limits: What are the rate limits, SLAs, and failure behaviors of third-party services in the path?
  • Organizational and regulatory constraints: Data residency, audit retention, team size, and on-call capacity all constrain the design as hard as any latency target.

These questions frequently reveal that a requirement conflicts with another one or with reality. A 99.99% target on a path that synchronously depends on a provider with a 99.9% SLA is a conflict. Surfacing it during requirements definition costs a conversation; surfacing it in production costs an incident.

A System Model: Requirements Mapped to Architecture

Consider a hypothetical checkout system with these quantified requirements:

  • POST /orders: p99 < 400 ms at 1,000 req/s peak; 99.95% request success.
  • No acknowledged order is lost, including on loss of one availability zone.
  • No more than one successful charge per order.
  • Inventory is never oversold.
  • Confirmation emails within 2 minutes at p99; stale by minutes is acceptable for order history views.
Client
  |
  v
Load Balancer                      (deadline starts: 400 ms budget)
  |
  v
Order API (stateless, N replicas)
  |  \
  |   \--> Payment Provider        (idempotency key = order_id, timeout 250 ms)
  v
Orders DB (primary + sync replica in 2nd zone)
  |  orders table + outbox table, written in one local transaction
  |  conditional inventory decrement (WHERE available >= qty)
  v
Outbox Relay
  |
  v
Event Stream / Queue               (at-least-once delivery)
  |
  +--> Email Worker                (dedup on event_id; 2 min freshness target)
  +--> Order History Projection    (eventually consistent read model)

Each structural choice traces to a requirement.

State ownership. The Orders DB is the single source of truth for orders and inventory. Placing both in one database lets the no-oversell invariant be enforced with a conditional update inside a local transaction, avoiding a distributed transaction entirely. This is a deliberate scaling boundary: it holds comfortably at the stated load, and it would need revisiting (for example, partitioning inventory by SKU) only if write throughput on hot items became the bottleneck.

Consistency boundaries. Everything inside the transaction is strongly consistent. Everything downstream of the outbox is eventually consistent. The outbox pattern exists because writing to the database and publishing to a broker are two separate systems; doing both in the request path means one can succeed while the other fails. Writing the event into the same transaction as the order, then relaying it asynchronously, turns a dual-write problem into an at-least-once delivery problem, which consumers handle through deduplication.

Failure boundaries. Email and history are off the critical path, so their failure degrades freshness rather than availability. The payment provider is on the critical path because the business requires authorization before confirming, which makes its availability part of the checkout ceiling. That is a stated tradeoff, not an accident.

Recovery behavior. If the API times out after the payment provider processed a charge, the client's retry carries the same order ID, and the provider's idempotency handling returns the original result. Duplicates are expected and absorbed rather than prevented.

Tradeoffs: Every Number Has a Price

Tightening a non-functional requirement is never just "better." It moves cost, complexity, or risk somewhere else.

Requirement changeWhat it buysWhat it costs
RPO from minutes to zeroNo acknowledged data loss on node or zone failureSynchronous replication adds write latency; write availability now depends on replica health
99.9% to 99.99% availability10x less tolerable failureRedundant dependencies, automated failover, progressive delivery, removal of single points of failure from the critical path
Strong to eventual consistency for a read pathRead scaling via replicas and caches; lower latencyUsers and downstream services observe stale data; application must tolerate it
Synchronous to asynchronous processingRequest latency decoupled from downstream speedQueue lag, ordering, duplicate delivery, and a second state machine to monitor
Lower p99 latency targetBetter tail experience, especially under fan-outTighter timeouts, fewer retries, possibly hedging or precomputation; more requests fail fast

Some of these requirements pull directly against each other. Synchronous cross-zone replication improves durability but worsens write latency and couples write availability to the replica. Aggressive retries improve the success rate for transient failures but multiply load during sustained failures. A cache improves latency and reduces database load but introduces a second copy of state with its own freshness contract.

A practical decision framework is to classify each requirement by the cost of violating it. Invariants whose violation causes financial or legal harm (double charges, data leakage, oversold inventory) justify strong consistency and coordination. Requirements whose violation causes inconvenience (a slightly stale order history, a delayed email) are where eventual consistency and asynchronous processing pay for themselves. Applying the expensive mechanism everywhere is not rigor; it is unpriced complexity.

Failure Modes and What Changes at Scale

Non-functional requirements should be stated relative to scale, because the dominant failure mode changes as load grows. The numbers below are illustrative.

At 10 requests per second, nearly any reasonable design works. A single database instance has enormous headroom, and the realistic risks are deployment errors, configuration mistakes, and dependency outages. Investing in sharding or multi-region active-active at this scale is premature; it adds failure modes the system does not yet need to defend against.

At 1,000 requests per second, resource exhaustion becomes the main threat. Connection pools, thread pools, and queue depths start to matter, and the slow-dependency chain described earlier becomes likely during any degradation. Timeouts, bounded queues, and retry budgets move from nice-to-have to required. Duplicate and reordered messages stop being edge cases because, at this volume, rare events happen daily.

At 100,000 requests per second, the problems become structural. A single primary may no longer absorb writes, which forces partitioning and with it cross-partition queries, rebalancing, and hot-key problems. Retry storms can take down healthy dependencies, so load shedding and admission control are needed. Small consistency anomalies, at this volume, become a steady stream of support tickets.

Across all scales, the failure modes worth enumerating in requirements are partial failure (some replicas or zones impaired), dependency latency, duplicated or reordered messages, stale reads, and cascading overload. A requirement that does not say how the system behaves under each of these leaves the behavior to chance.

Engineering Principles for System Design Requirements

  1. A requirement that cannot fail a test is not a requirement. If no measurement could show it was violated, it will not constrain the design.

  2. Non-functional requirements are architectural decisions in disguise. Choosing 99.99% availability or RPO zero commits the team to specific mechanisms and operational practices before a line of code is written.

  3. Availability composes; it does not average. A service cannot be more available than the product of its synchronous dependencies unless it is designed to operate without them.

  4. Latency targets are budgets that must be divided. Every timeout, retry, and synchronous call spends from the same budget, and the budget should be encoded in deadlines, not left to library defaults.

  5. Every guarantee needs an owner. "Exactly once" and "never oversell" must be assigned to a specific component that enforces them, or they are enforced by no one.

  6. Distinguish invariants from preferences. Spend coordination and consistency on the things that must never be wrong, and let the rest be eventually correct.

  7. Requirements are relative to scale. A design should match the load it must handle in the foreseeable future, not the load that makes for an impressive diagram.

Final Thoughts

The distinction between functional vs non-functional requirements is often taught as a vocabulary exercise. Its real value is as a design discipline. Functional requirements tell you what to build. Non-functional requirements tell you what can go wrong, how often it is allowed to go wrong, and what the system must do when it does. That second category is where architecture actually happens.

Engineers who are good at this do not start by choosing a database or a queue. They start by asking where state lives, which invariants must hold, which dependencies sit on the critical path, how failure propagates, and what each quantified target will cost. The technology choices then follow from the constraints, rather than the constraints being discovered after the technology was chosen.

These are the kinds of engineering problems that sit at the center of modern software systems: understanding where state lives, how components interact, and what happens when the assumptions between them stop being true. Treating requirements as precise, testable guarantees is the first and cheapest place to get that right.

Key Takeaways

  • Functional requirements describe user capabilities under normal conditions; non-functional requirements describe the guarantees the system must keep when those conditions degrade, and they drive most architectural decisions.
  • A useful non-functional requirement specifies a metric, a target, a measurement window, and a load condition, such as "p99 < 300 ms at 2,000 req/s over rolling 5-minute windows."
  • Moving from 99.9% to 99.99% availability reduces tolerable downtime from roughly 43 minutes to roughly 4 minutes per month, which typically requires automated recovery and removing single points of failure from the critical path.
  • Serial dependencies multiply availability, so five synchronous 99.9% components cap a request path near 99.5% regardless of how reliable the service itself is.
  • Latency must be specified as percentiles because fan-out turns component tail latency into aggregate median latency.
  • "Exactly once" cannot be guaranteed by the network; it must be restated as an invariant enforced by idempotency keys and deduplication at the point of side effect.
  • Stronger guarantees (zero RPO, strong consistency, tighter latency) always move cost into latency, coupling, or operational complexity, so they should be reserved for invariants whose violation causes real harm.

Get insights like this in your inbox

Join thousands of engineers receiving practical engineering, Cloud, Distributed Systems, Infrastructure and AI engineering insights.

No spam. Unsubscribe anytime.

← Back to Blog