Non-Functional Requirements (with Examples)
What’s the difference between software that works and software that works well? Non-functional requirements.
Consider the following example.
We’ve been hired to build an online store for a local brick-and-mortar store. We’ve gone ahead and applied the feature/use case-driven approach to design popularized by XP. We’ve done that process of identifying the use cases, planning and estimating them with user stories, and thinking about how we can write those stories as tests with BDD. Looking back at what we’ve done, we’ve acceptance tested the use cases, integration tested the infrastructure code that interacts with external APIs and the outside world, and even did a little bit of end-to-end testing to ensure things work from front to back.
Unfortunately, upon going live, we’re surprised hear about a myriad of complaints from the customer.
The customer calls us and says:
- “The cart page takes around 10 seconds to load — it’s way too slow! If it’s already this slow under regular traffic, I’m not sure what’s going to happen when we have our boxing day sale. Will the site just go down completely?”
- “We keep having to refund orders! Customers are purchasing products that are out of stock, so I’m not sure how you did the sync to our backend inventory but it’s either taking too long to sync or it’s not syncing at all.”
- “The site looks weird on mobile.”
- “Customers are confused about how to get to the page where they can see the status of their orders.”
- “The animations moving between pages feels kind of choppy. I was expecting it to be smoother. Can you fix that?”
Dang. Where did we go wrong?
Well, behind each of these disgruntlements lies a missed requirement — a non-functional requirement. And non-functional requirements are an incredibly important piece of the puzzle that is software design.
In this post, we’re going to discuss:
- What non-functional requirements are and why knowing them can help us better design systems that meets the needs of the customer and end users.
- How non-functional requirements compare to functional ones.
- Best practices to handle non-functional requirements.
Where functional requirements describe what the system should do (ie: the use cases or features), non-functional requirements (sometimes called constraints or behavioural requirements) describe what the system should be (ie: the system level properties).
With functional requirements, if we’re following the feature-driven folder structure, it can be relatively easy to trace features down to individual slices of functionality or modules within our application.
Non-functional requirements, however — these are system-level properties. Since they describe the system as a whole it’s a lot harder (and sometimes impossible) to trace them down to a location in the codebase.
There’s no one single definitive list of valid non-functional requirements. Some of them can also been seen as related or within umbrellas of each other (for example: we can define Performance or Speed as NFRs within Efficiency).
To get us started, here’s a short list.
- Capacity — How much data can we store? Will we ever run out of storage space?
- Regulatory — Do local, regional, provincial or state laws or regulations that dictate some aspect of what we’re legally allowed to do?
- Portability — Which browsers, operating systems and versions should we expect the software to work on? Is it cross-platform?
- Reliability — How often can we expect the system to be available to use? How does it handle critical failures? How long does it take for a system to come back online after a failure?
- Performance — How fast should requests process? How many requests can we handle at any given time? What should we expect for response times when the system is under heavy load?
- Localization — Will the system adapt to measurement systems, timezones, and other locally specific concepts in other states or provinces?
Lets say that a functional requirement in a trading application was to make sure that when users execute the
getOffers use case, they’re presented with a list of offers, then a larger-picture non-functional requirement may state:
- Scalability, performance, efficiency: “The website should process each request within 4 seconds or less 99% of the time.”
- Scalability, performance: “In standard network conditions, the website should fully load in less than 5 seconds when the total number of simultaneous users on the website are greater than 50,000.”
- Security, data integrity, capacity: “Every unauthorized request to a resource must be logged and stored for audit over the next 5 years.”
Because there are so many different non-functional requirements out there, a great way to think about them is to consider them to be either either execution qualities or evolution qualities. Allow me to explain the difference.
- Execution qualities: These are aspects which can only be observed at runtime such as security, usability, and efficiency (performance, speed).
- Evolution qualities: These are aspects that have to do with how it affects our ability to evolve the code and structure over time such as if the code is testable, flexible, maintainable, and scalable.
Let’s make sure we really understand the differences between the two.
|Describes what the system should do
|Constrains the functional requirements and describes “how the system should be”
|No, but some are more important than others
|As a use case / feature - component level
|As a system-level property against the system as a whole
|Ease of definition
|More difficult (it can be difficult to make qualitative properties quantitative — we sometimes have to use indicators instead).
|Unit, integration, acceptance, end-to-end tests
|Performance, stress, security, usability testing (for example)
|Given a user has not created an account, when they register, a new users should be created and an email should be sent to them.
|(1) The site should be able to process 99.9% of requests in less than 6 seconds. (2) Emails should be sent no later than 20 minutes after the triggering event.
My first startup project was used by few hundreds users. It wasn’t until it received a moderate amount of press on a popular website that I learned it was not cut out for real-world production traffic. With thousands of people signing up at the same time, the website — hosted on a tiny AWS EC2 micro instance, unable to stand up to the performance, efficiency, and reliability non-functional requirements — died a tragic death and left many users annoyed... never to return again.
Often times, even if we get all the features working, non-functional requirements are the key factor that dictate if a project will succeed or fail. It’s taken me some time to realize this, but it’s very much true.
What good is a slow social networking website? Who’s going to play a game that crashes all the time? Is the project really a success if it’s written without tests and every time we add or change a feature, we fear breaking an existing feature in a distant module? What if people don’t even know how to use it?
Non-functional requirements provide hints to what sorts of architectural components (like queues and caches) and patterns (design patterns and architectural patterns) we’ll need in order to successfully get the system to be a certain way — to have the design criteria we need.
For example, a media-heavy site like Instagram or YouTube allows users to upload media. That’s the functional requirement. Non-functionally though, we’d like to afford users the capability to continue to move around the site while their media is being uploaded.
This simple usability non-functional requirement introduces the need for some component capable of decoupling the user-facing command of
UploadMedia from the work to be done (such as processing the media, encoding it, annotating it, adding subtitles, etc). Therefore, it’s likely we’d benefit from an event-based architecture and the addition of a Job/Task Queue as an important architectural component.
Filling in the non-functional gaps with Object Design: Do I just automatically know what sorts of components we need? While a familiarity of patterns, tools, and services and the sorts of problems they solve helps, there is a structured design method to discover what sorts of components need to either be invented or integrated into our application architecture. It’s called Object Design (or as I like to say how to do object-oriented programming properly). Object design teaches us how to discover and divvy up the necessary requirements (both functional and non-functional requirements) into neighbourhoods of collaborating objects. In Part V — Object Design with Tests from solidbook.io, we learn Responsibility-Driven Design and foundational skills for system design.
A large reason why we use things like:
- Tests (acceptance, E2E, integration, unit)
- Design patterns
- Design principles
- Architectural styles
- Architectural patterns
- Domain-driven design
... is because they lead to more testable, flexible, and maintainable code. The evolution qualities. That’s pretty much the goal for us web developers focused on taming backend complexity. As a result, following the many good coding practices listed above leads to code can that can be more easily evolved, lives longer, is more understandable, and so on.
- Uncover non-functional requirements early: Because of the impact NFRs have on architecture, it’s important to discover them early. Once you know the functional requirements, ask the customer (or yourself) probing questions to determine what non-functional requirements exist to constrain the functional ones. How fast should it be? How do users expect this to behave? What will happen if users have a lot of data in their account? Are there auditing requirements? What could make this project fail? What does affordability look like for you? How easy will it be to change? Can we expect users to know what to do?
- Make requirements as measurable as possible: Here’s an example of a non-functional requirement that you can’t really test or measure: “it should be fast as hell”. What if we made that quantitative and said “it should respond to 95% of requests within 3 seconds”? That’s better, isn’t it?
- Use popular products as a quality baseline: Every couple of years, users come to expect better website experiences. Faster, more real-time, more of that wow-factor and so on. To get a baseline for the non-functional requirements, ask customers to tell you what they like from other websites. It could be just that it feels consistent or has a lot of whitespace. These are all constraints on the functional requirements.
- Non-functional requirements describe what the system should be instead of what the system should do. It’s about system-level properties.
- Non-functional requirements can be divided into two rough categories: execution qualities and evolution qualities.
- We should learn non-functional requirements because failing to meet them is one of the main reasons why projects fail, they have a massive influence on architecture, and they make a solid case for good coding practices like TDD and DDD and more advanced concepts like design patterns and architectural styles.
There's a research paper I'm currently in the middle of reading for the upcoming section in solidbook.io titled "Part V: Object-Oriented Design (with Tests)" -- it's called "From Non-Function Requirements to Design through Patterns" by Eric Yu and Daniel Gross from my neighboring University of Toronto.
In the paper, they state that design patterns are excellent tools for handling particular NFRs even though they often come with tradeoffs. The paper presents three design patterns and shows how they can be used to solve some NFRs.
We've known this to be true. Just look at Redux: the front-end state management library centered around the Observer Pattern. What properties would a system architected around the observer pattern gain? Consistency, accuracy, generality, expandability, traceability, and more! We tend to like the elegance of this decoupled publish-subscribe pattern for so many reasons. It makes logging, auditing, tracking events, and so on easier. In fact, if we zoom out and think about CQRS and the fact that we use Domain Events in a Hexagonal Architecture, you will also see the Observer Pattern - but at an architectural level!
If you're a regular reader of this blog, you're passionate or curious about how to design systems in a testable, flexible, maintainable way. In "Chapter 24. An Object-Oriented Architecture" from solidbook.io, we delve into a common object-oriented architecture that works for most web developers.
Architecture is meant to support the functional and non-functional requirements. A good architecture is one that does this. In object-oriented web or business applications, perhaps one of the most important non-functional requirements is the ability to test business logic complexity effectively. I argue that a layered architecture is the superior technique. Why? It lets us implement functional requirements in a test-first and domain-driven way.
Liked this? Sing it loud and proud 👨🎤.
Stay in touch!
Enjoying so far? Join 15000+ Software Essentialists getting my posts delivered straight to your inbox each week. I won't spam ya. 🖖
View more in Object-Oriented Analysis
You may also enjoy...
A few more related articles
Want to be notified when new content comes out?
Join 15000+ other Software Essentialists learning how to master The Essentials of software design and architecture.