What does it mean to decompose a system by isolating volatility (rather than by functional decomposition), and how do you identify the volatile areas?
answer
- hide decisions likely to change (Parnas 1972)
- functions change; axes of variation are more stable
- volatility over time / across customers / across environments
- find it via churn hotspots + co-change + roadmap
- rule of three; config beats an interface for volatile values
basics
~20 sInstead 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 sVolatility-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
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.
Contrast with functional decomposition, give concrete axes of variation, and connect to dependency inversion and ports-and-adapters.
Show how you find volatility with evidence — churn hotspots, co-change, roadmap, tenant variation — and discuss speculative generality and leaky first-implementation interfaces.
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.