Non-Functional Requirements and Modularity

When software engineers talk about requirements, we’re often talking past each other. Functional requirements are easy to write, decompose…

When software engineers talk about requirements, we’re often talking past each other. Functional requirements are easy to write, decompose, and assign. Non-functional requirements (NFRs), on the other hand, seem to live in the shadows: they are everywhere, affect everything, and yet belong nowhere in particular.

This post explores why non-functional requirements — performance, security, usability, resilience, and others — are fundamentally anti-modular, and what that means for software design.

Functional vs. Non-Functional: A False Dichotomy?

At first glance, it’s tempting to split software requirements into two clean categories:

  • Functional requirements: What the system does (e.g., “Display user profile,” “Send invoice email”).
  • Non-functional requirements (NFRs): How the system behaves (e.g., “Load profile within 300ms,” “System should be secure against injection attacks”).

But this neat categorization hides a deeper challenge: NFRs don’t compose cleanly. Unlike functions that can be tested, sliced, and assigned to modules, non-functional aspects are cross-cutting concerns. They are emergent properties, not features.

Let’s take a look at a few common NFR’s to understand why they are orthogonal to modularity.

Performance

Consider a common performance spec:

“The screen must load in under 300ms, 99% of the time.”

Sounds simple, right?

But this requirement is not atomic. It decomposes into many layers:

  • Time to retrieve data from backend APIs
  • Time spent in business logic transformation
  • Time to render components in the UI
  • Time to load and parse third-party scripts

And each of these decomposes further — database query latency, caching strategies, thread scheduling, network reliability, and so on.

This cascade leads to a non-local dependency chain, where:

  • The performance of a high-level action is the sum (and sometimes product) of tiny latencies in dozens of modules.
  • No single module owns the performance requirement — but all can contribute to its failure.

Performance, therefore, is not modular. It emerges from interactions, not implementations.

Security

Security is another area that cannot be reasoned using modularity.

Take the idea of a sandbox: a restricted environment designed to enforce security properties by limiting access to resources.

Sounds simple in theory — until you try to build one.

The effectiveness of a sandbox depends not just on what it allows, but what it prevents. And that “prevention” is not owned by a single module. Instead:

  • A sandbox requires cooperation from the OS, runtime, and application code.
  • One insecure escape hatch — an exposed file descriptor, a shared memory pointer, or an unchecked deserialization method can invalidate the entire construct.
  • Security is broken by the weakest link, not ensured by the strongest one.

Much like performance, security is not composable in a clean additive way. You can’t simply “add a secure component” and make a system secure. The system must close gaps across boundaries.

Usability

Designers know this instinctively:

highlighting everything highlights nothing.

Suppose you bold one label on a screen to improve visibility. Suddenly:

  • Nearby text looks weaker.
  • Layout balance is disrupted.
  • Color contrast may need to shift elsewhere.
  • Accessibility scores could be affected.

Small local changes have global perceptual effects.

That’s the problem with usability as an NFR: it’s subjective, relational, and system-wide. You can’t assign “contrast improvement” to a single module. You can’t A/B test a single component in isolation and conclude that the entire system is more usable.

Usability is not about individual components; it’s about the relationship between them — timing, positioning, perception, feedback loops.

The Myth of Locality in Non-Functional Requirements

Why are NFRs so hard?

Because most of software engineering is optimized for local reasoning:

  • We write modular code.
  • We write unit tests for isolated functions.
  • We define interfaces and contracts between modules.

This works well for functionality. But NFRs don’t live in one place. They require global awareness, cross-cutting constraints, and feedback between parts.

Here’s how this plays out:

  • Performance :Emerges from latency and coordination across all layers
  • Security: Depends on weakest points across trust boundaries
  • Usability: Perceived through holistic interaction and visual relationships
  • Availability: Requires coordination of failover, retries, observability
  • Scalability: Arises from system topology, not component behavior

Concepts To The Rescue

So how do we manage the anti-modularity of non-functional requirements?

One powerful approach is to design in terms of concepts, rather than just components. A concept is more than a class, function, or module. It’s a coherent unit of purpose, state, behavior, and constraint. It captures what something is, what it does, and the rules that govern its role in the system.

Instead of asking “What components do I need?” ask “What concepts make this system meaningful?”

In a previous post, I explored how this shift enables more flexible and resilient software design. But let’s now look at how it directly helps with non-functional concerns like performance, security, and usability.

Example 1: Upload as a Concept

In a component-driven approach:

You might break this into:

  • UploadFormComponent
  • UploadService
  • FileStorageAdapter
  • ProgressBarComponent

Each component has a clear boundary — but none of them own the non-functional requirements. Where does the logic live for:

  • Rejecting oversized files?
  • Handling flaky connections?
  • Displaying progress and retry options?
  • Applying virus scanning or content restrictions?

These concerns get scattered, duplicated, or forgotten.

In a concept-driven approach:

You define Upload as a first-class concept, which owns:

  • The user interaction (progress, errors, cancel)
  • The system contracts (size limits, file types, scanning)
  • The runtime behavior (retry policy, queuing, latency targets)

Now, when you think about performance, you can model upload latency and retry behavior as part of the concept itself. When you think about security, you can embed pre-upload scans and access control directly into the concept, not as scattered filters.

The concept holds all the invariants together, so non-functional behavior doesn’t leak across the system.

Example 2:User Session as a Concept

In a traditional component model:

You might have:

  • SessionManager
  • TokenValidator
  • ActivityTracker
  • IdleTimeoutScheduler

Each manages a piece of the puzzle. But again, non-functional concerns are fragmented:

  • Who tracks session expiration policy?
  • Who logs out users automatically after inactivity?
  • Who handles geographic restrictions or anomaly detection?

These often get bolted on, and ownership is unclear.

In a concept-driven model:

You define a UserSession concept that explicitly governs:

  • Lifetime (start, renew, expire)
  • Boundaries (IP range, device trust)
  • Side effects (activity logs, audit trail)
  • Failure modes (force logout, token revocation)

Now, if your system requires resilience (graceful handling of expired tokens) or security (session hijack detection), the UserSession concept is the natural place to embed those behaviors.

You stop thinking about token parsing, and start thinking about what a secure session means in your system.

Why This Helps

When you think in terms of concepts:

  • You gather all non-functional behavior into meaningful clusters, not arbitrary components.
  • You can test, validate, and evolve non-functional constraints as part of a cohesive model.
  • You allow orthogonal concerns like performance or security to be designed and evaluated within the scope of a concept, not by hacking together global middleware.

More importantly, concepts often align better with domain language, making them easier to communicate across teams — especially between engineering, product, security, and UX.

Designing with concepts doesn’t make NFRs disappear. But it gives them a home — a place where they can be reasoned about, tested, evolved, and enforced.

In other words, it brings back modularity through meaning, not structure.

Designing With NFRs, Not Around Them

You can’t “tack on” non-functional requirements after the fact. Instead, you must:

  • Elevate them to first-class design goals
  • Model them across layers, not within a single component
  • Instrument and observe, since behavior must be validated in the wild
  • Accept trade-offs, because improving one often hurts another (e.g., security vs usability, performance vs resilience)

Conclusion: Engineering Beyond the Box

Non-functional requirements aren’t invisible — they’re everywhere. But they don’t show up in our module diagrams or class hierarchies. They leak through seams. They emerge from interactions. They whisper through user complaints and performance dashboards.

To reason about NFRs is to reason across boundaries: between modules, teams, design disciplines, and abstraction layers.

If functional design is about assembling the pieces, then non-functional design is about tuning the orchestra.

And if you’re tired of the pain of treating non-functional requirements as afterthoughts, consider this: maybe it’s time to stop thinking in terms of components and start thinking in terms of concepts.

Read more