skip to content

What does it mean to decompose a system by isolating volatility (rather than by functional decomposition), and how do you identify the volatile areas?

level: seniorimportance: should knowfreq 38%

answer

  1. hide decisions likely to change (Parnas 1972)
  2. functions change; axes of variation are more stable
  3. volatility over time / across customers / across environments
  4. find it via churn hotspots + co-change + roadmap
  5. rule of three; config beats an interface for volatile values

basics

~20 s

Instead of creating a module per business function, you create modules around the things most likely to change — a payment provider, a tax rule, a storage technology — and hide each behind a stable interface, so future changes stay inside one module.

solid answer

~50 s

Volatility-based decomposition (argued by Juval Löwy, and rooted in Parnas's 1972 information-hiding paper) says: encapsulate the *design decisions likely to change*, not the steps of the current business process. Functional decomposition creates a module per verb (`CreateOrder`, `ShipOrder`); because business processes change often, that structure changes with them, and every requirement change ripples. Volatility-based decomposition asks "what varies — across customers, over time, across environments?" and puts each axis of variation behind a stable abstraction: payment providers, notification channels, tax jurisdictions, storage engines, pricing policy. Identify volatility from evidence: version-control churn/hotspots, the roadmap, variation across customers or regions, regulatory pressure, vendor dependencies, and explicit "we might switch X" statements. Trade-offs: abstractions guessed wrong are pure cost (speculative generality, wrong-level interfaces leaking the first implementation's shape), so prefer the *rule of three* — abstract on the second or third real variation — and note that some volatility is best absorbed by data/configuration rather than by a new module.

go deeper

for a junior

Say that modules should be built around what is likely to change (payment provider, storage), hidden behind an interface, so changes stay in one place.

for a middle

Contrast with functional decomposition, give concrete axes of variation, and connect to dependency inversion and ports-and-adapters.

for a senior

Show how you find volatility with evidence — churn hotspots, co-change, roadmap, tenant variation — and discuss speculative generality and leaky first-implementation interfaces.

for a principal

Frame it as deliberately choosing which changes get a small blast radius, reconcile it with domain-meaningful boundaries (DDD contexts), and decide per case among code abstraction, configuration/data, and organizational ownership.

## The idea **Parnas (1972), "On the Criteria To Be Used in Decomposing Systems into Modules"**: modules should be chosen to hide *design decisions that are likely to change*, not to mirror the steps in a flowchart. Juval Löwy later popularized the phrase **"volatility-based decomposition"** and contrasted it explicitly with **functional decomposition**. - **Functional decomposition** — one module per business function/verb: `Register`, `Checkout`, `Refund`, `Ship`. Intuitive, matches the requirements document. - **Volatility-based decomposition** — one module per *axis of change*: `PaymentProvider`, `TaxPolicy`, `NotificationChannel`, `IdentityStore`, `PricingRules`. ## Why functional decomposition ages badly The requirements document describes today's process. Business processes are exactly the thing that changes. If your module map is a snapshot of the process, every process change is a structural change: new modules, changed interfaces, ripple through callers. Two named symptoms: - Modules multiply as the process grows (a module per new step), and orchestration logic bloats. - Common behavior is duplicated across functional modules (`Checkout` and `Refund` each carry their own payment-provider details), so a provider change is a multi-module change. It also tends to produce a **decomposition of the requirements rather than of the design** — each module needs all the others because a real transaction crosses several verbs. ## What volatility means, concretely Volatility = variation. Along three dimensions: 1. **Over time** — will this change in the next 1–3 years? (tax rates, compliance rules, provider APIs) 2. **Across customers/tenants/regions** — do different users need different behavior? (branding, currency, legal rules, SSO providers) 3. **Across environments/runtime** — dev vs. prod, on-prem vs. cloud, mock vs. live (storage backend, message broker, clock, random source) Anything that varies along any of these is a candidate for encapsulation behind a stable interface. ## How to find it — evidence, not intuition - **Version-control churn / hotspot analysis.** Rank files by commit count and by number of distinct authors. High-churn areas are empirically volatile; Adam Tornhill's *behavioural code analysis* work formalizes this. Combine churn with complexity to prioritize. - **Co-change clusters.** Files that always change together belong to the same volatility axis, wherever they currently live. - **The roadmap and the sales pipeline.** "We're adding a second payment provider next quarter", "enterprise wants their own SSO" — future variation stated by the business. - **Variation across existing customers/config.** Every `if (tenant == X)` and every feature flag is volatility already leaking into code. - **External dependencies.** Anything owned by someone else (vendor API, regulator, OS, third-party format) changes without your consent — a strong isolation candidate. - **Interview question to the business:** "What will still be true about this in five years?" What survives is stable and can be depended on; what doesn't must be hidden. ## Applying it Once you have an axis of variation: 1. Define a **stable abstraction** in the vocabulary of the *client's need*, not the current implementation. `PaymentGateway.authorize(amount, instrument)` — not `StripeChargeRequest`. 2. Depend on the abstraction from the stable core (this is the Dependency Inversion Principle, the D in SOLID: high-level policy must not depend on low-level detail; both depend on abstractions). 3. Keep the volatile detail in an adapter at the edge (ports and adapters / hexagonal). 4. Prove the abstraction by having (or writing) at least a second implementation, including test doubles. ## Trade-offs and failure modes - **Speculative generality.** A plugin framework for a variation that never arrives is pure carrying cost — extra indirection, more code, slower comprehension. YAGNI applies. - **Leaky abstraction shaped by the first implementation.** An interface derived from one vendor's SDK (`chargeWithIdempotencyKeyAndSetupIntent`) will not fit the second vendor. Antidote: design the interface from *the caller's need*, and validate it against a second real implementation (rule of three: hard-code once, tolerate twice, abstract on the third). - **Wrong granularity.** Isolating something that never varies costs boundary tax forever. Isolating too coarsely (one `ExternalStuff` module) hides nothing. - **Not everything volatile needs a module.** Frequently changing *values* (tax rates, thresholds, copy text) belong in data or configuration, not in code behind an interface. Frequently changing *decision logic* may belong in a rules engine or feature flags. Choose the cheapest mechanism that localizes the change. - **All-volatility, no-domain designs.** Löwy's approach is criticized for producing generic, abstract module names that hide the business domain; DDD's bounded contexts are the counter-argument that boundaries should also carry domain meaning. In practice, combine: contexts define domain-meaningful boundaries; volatility analysis chooses which details inside them get abstracted. ## Blast radius The payoff is expressed as **blast radius**: the set of components that must be inspected, changed, retested, and redeployed when a given change occurs. Volatility-based decomposition is the deliberate act of arranging boundaries so that the *frequent* changes have a small blast radius, accepting a larger radius for changes you have judged rare.

  • How do you avoid speculative abstraction while still isolating volatility?
    Require evidence before abstracting: a second real implementation, an explicit roadmap item, or observed churn — the rule of three. Until then, keep the detail concrete but *contained* in one place (a single file or class), so introducing the interface later is a local refactor rather than a rewrite.
  • When should volatility be handled with configuration or data rather than a new module?
    When the variation is in *values or thresholds* rather than behavior — tax rates, limits, copy, feature toggles. Data-driven variation changes without a deploy and without new code paths. Reserve modules and interfaces for variation in *behavior or protocol*, where the shape of the interaction differs.
  • What is the main criticism of a purely volatility-driven decomposition?
    It can yield abstract, domain-blind module names (Manager/Engine/Accessor) that make the business hard to read and encourage a generic framework nobody needs. The usual correction is to set domain-meaningful boundaries first (bounded contexts / capabilities) and apply volatility analysis to choose what gets abstracted inside them.

A building isolates what changes: partition walls and cabling are designed to be moved, load-bearing structure is not. You don't lay out a building around today's seating chart — you lay it out so tomorrow's seating chart is a cheap change.

context