Functional and Non-Functional Requirements
What the system does versus what must hold under load, and why every latency and availability target is a budget.
functional: what it does | non-functional: p99 latency at a stated load, availability, durability, consistencyFunctional requirements are what the system does: a user can post, a follower sees the post, an upload returns a URL. Non-functional requirements are what must stay true while it does that under load — latency, availability, durability, consistency. The split earns its keep because the functional list decides which boxes exist and the non-functional list decides what goes inside them.
State them as a number at a percentile
"Fast" is not a requirement. Amazon's Dynamo paper states internal SLAs at the 99.9th percentile — for example, a response within 300ms for 99.9% of requests at a peak load of 500 requests per second — because a mean hides exactly the slow requests that belong to your most active users. A latency target without a percentile and a load attached cannot be designed against.
Every target is a budget
Availability converts straight into downtime you are permitted to spend; the Google SRE books call the remainder the error budget, and 99.9% over a 30-day month leaves 43.2 minutes. Pushing to 99.99% leaves 4.32 minutes, which buys a second region and automated failover rather than a configuration change. Durability is paid for in write latency, because an acknowledged write has to be on more than one machine before you return 200. Strong consistency is paid for in availability during a network partition, and in latency when there is not one.
Say the price out loud as you set each target. A candidate who asks for four nines and strong consistency everywhere has not set requirements, they have dodged the question.
Example
const MINUTES_PER_30_DAYS = 30 * 24 * 60; // 43_200
// Availability target -> downtime you are allowed to spend. The Google SRE books
// call the remainder the error budget.
const errorBudgetMinutes = (target) => MINUTES_PER_30_DAYS * (1 - target);
errorBudgetMinutes(0.99); // 432 — a long afternoon
errorBudgetMinutes(0.999); // 43.2 — one bad deploy, rolled back fast
errorBudgetMinutes(0.9999); // 4.32 — no human is in the loop at this number
const requirements = {
functional: [
'POST /posts creates a post and returns its id',
'GET /feed returns the 20 most recent posts from accounts the caller follows',
'POST /follow makes one account follow another',
],
nonFunctional: {
// The Dynamo paper's example SLA shape: a latency, a percentile, AND a load.
feedRead: { percentile: 99.9, latencyMs: 300, atQps: 500 },
postWrite: { percentile: 99.9, latencyMs: 500, atQps: 100 },
availability: 0.999, // 43.2 min / 30 days, costs: multi-AZ, no multi-region
durability: 'no acknowledged post is lost: replicated before the 200 is returned',
consistency: 'feed may be 30s stale; follow takes effect for the follower immediately',
},
};When to use it
- Turning "it should be highly available" into a number with a window
- Writing the non-functional section of a design doc your team will be held to
- Pushing back on a "must be real time" requirement with a staleness budget
- Checking whether an added synchronous dependency breaks an existing SLO
More examples
Availability target to the redundancy it actually requires
The serial-dependency multiplication is the detail candidates miss: adding a synchronous call lowers the ceiling of the whole path. Use it when someone proposes putting one more service in the request path.
// Nines are not a config value. Each step is a different architecture, and the
// interviewer is checking whether you know what you just asked for.
const cost = {
0.99: { downtime30d: '432 min', needs: 'one instance, a monitored restart' },
0.999: { downtime30d: '43.2 min', needs: 'N instances across availability zones, health-checked LB' },
0.9999: { downtime30d: '4.32 min', needs: 'multi-region, automated failover, no manual step' },
};
// Dependencies multiply: a request that must touch three independent 99.9%
// services cannot itself be 99.9%.
const serial = (...targets) => targets.reduce((acc, t) => acc * t, 1);
serial(0.999, 0.999, 0.999); // 0.997002... -> ~129 min / 30 days, not 43.2Durability stated as an acknowledgement rule
Three data types in one system, three different ack rules, each with its latency written next to it. This is the form a durability requirement has to take before it constrains a design.
// "Durable" means nothing until you say what has to be true before you return 200.
// Dynamo's (N, R, W): with R + W > N, a read touches at least one replica that
// holds the newest write. The paper's common configuration is (3, 2, 2).
const quorum = { N: 3, W: 2, R: 2 };
const readsSeeLatestWrite = quorum.R + quorum.W > quorum.N; // true
const ackPolicy = {
payments: { waitFor: 'commit on primary + 1 synchronous replica', costMs: 'adds a round trip' },
posts: { waitFor: 'commit on primary, replicas catch up asynchronously', costMs: 'none' },
viewCounts: { waitFor: 'nothing — buffered in memory, flushed in batches', risk: 'lost on crash' },
};
Discussion