| name | lowy |
| description | Volatility-based decomposition review.
Trigger when:
- Evaluating module boundaries against axes of change and business volatility.
- Prompt contains: /lowy, volatility, axis of change, design decomposition, boundary.
|
Volatility Decomposition Review
Evaluate module boundaries and decomposition decisions using Juval Löwy's volatility-based decomposition framework. The core question: do your boundaries encapsulate axes of change, or do they just group related functionality?
[!NOTE]
This workflow is the temporal half of the code-spacetime review pair. It reads code as a world-line — what will pull apart, and when? For the spatial half — what's tangled right now? — see /hickey. Run both for full coverage. See engineering.md §8.1.
Attribution: Adapted from Juval Löwy's Righting Software (2019), building on David Parnas, "On the Criteria to Be Used in Decomposing Systems into Modules" (1972). Workflow structure adapted from Srid Ratnakumar's implementation. See also: Volatility-Based Decomposition (book excerpt).
Core Distinction
Functional decomposition groups code by what it does (UserService, PaymentController, AuthModule). Volatility-based decomposition groups code by what is likely to change — and encapsulates each axis of change behind a stable interface.
"This principle governs the design of all practical systems, from houses, to laptops, to jumbo planes, to your own body. To survive and thrive they all encapsulate the volatility of their constituent components." — Löwy
Löwy's electricity analogy: a house's power supply has enormous volatility (AC/DC, 110v/220v, 50/60Hz, solar/grid/generator, wire gauges). All of it is encapsulated behind a receptacle. Without that encapsulation, you'd need an oscilloscope every time you plugged something in. The receptacle is the stable interface; the volatility behind it can change without affecting consumers.
Functional decomposition maximizes the blast radius of change. When boundaries track functionality rather than volatility, a single change cuts across multiple modules. "Functional decomposition is as diverse as the required functionality across all customers and points in time. The resulting huge diversity in the architecture leads directly to out-of-control complexity." Volatility-based decomposition contains the grenade in the vault.
Two Types of Volatility in Business Logic
Löwy identifies two independent axes within any workflow:
-
Sequence volatility — the order of steps in a workflow can change independently of what those steps do. Different customers or use cases may require different orchestrations of the same activities. This volatility belongs in orchestrators (Löwy's "Managers").
-
Activity volatility — how a specific activity is performed can change independently of the sequence it appears in. There may be an unknown number of ways to do the same activity (different algorithms, providers, strategies). This volatility belongs in strategy components (Löwy's "Engines").
Conflating these two — putting orchestration logic and activity logic in the same module — means a change to either axis ripples into the other.
Variable vs. Volatile
Not everything that varies is volatile. Löwy makes a critical distinction: adding an attribute to a data model is variable but not volatile — the architecture won't suffer. "If you cannot clearly state what the volatility is, why it is volatile, and what risk the volatility poses in terms of likelihood and effect, then you need to look further." Decomposing around things that merely vary (rather than things that are genuinely volatile) produces over-engineered boundaries that add cost without containing real change.
The Evaluation Process
For every module boundary, service split, or new abstraction in the code under review:
1. Name the Volatility
What is likely to change behind this boundary? Be specific — not "requirements might change" but "the payment provider, the auth protocol, the notification channel." If you can't name concrete axes of change, the boundary may be arbitrary.
If the project has explicitly enumerated its own areas of volatility (the term of art Löwy uses throughout Righting Software), evaluate them against Löwy's bar:
| Area of volatility | What changes | Why volatile (likelihood × effect) | Expected encapsulation |
|---|
Do two things with each declared volatility: (a) re-apply Löwy's bar — "state what the volatility is, why it is volatile, and what risk the volatility poses in terms of likelihood and effect" — and challenge any that fail it (Löwy: "It is important to discuss the volatility candidates this way and even challenge them"); (b) audit whether the boundaries under review actually encapsulate the surviving volatilities in a single component, rather than spraying or leaking them across modules.
Speculative volatility is not volatility. A change scenario counts only if it has happened before, is on a roadmap, or is a near-certain consequence of the domain (e.g. "payment providers change" in e-commerce). "What if we swap color spaces" in an app that has never swapped color spaces is speculation, not an axis of change. Löwy's framework is about observed or plausible volatility — designing for hypothetical change is over-engineering, not encapsulation.
Weak volatility may not deserve its own boundary. Some volatilities are real but too minor to justify a separate component. Ask: does this volatility justify the cost of an additional boundary, or can it be folded into an existing one?
2. Classify the Volatility
Is the volatility about sequence (the order/orchestration of a workflow) or activity (how a specific step is performed)? These are independent axes and should be encapsulated separately. A module that mixes both will be modified for two unrelated reasons.
Also check for domain decomposition — boundaries drawn around domain entities (ProjectService, TradesmanModule, AccountsManager) rather than around axes of change. Domain decomposition is functional decomposition wearing a domain hat. Löwy warns: it creates ambiguity about who does what and when, duplicates functionality across domain lines, and is nearly impossible to validate against use cases.
3. Functional vs. Volatility Boundary
Does this boundary exist because the code does something different (functional), or because what's behind it changes independently (volatility)? Functional boundaries look clean on day one but fracture under change. A UserService that groups all user operations is functional decomposition — the volatility of auth, profile data, and notification preferences are unrelated axes of change jammed behind one boundary.
The naming test. Löwy uses naming conventions as a diagnostic. Orchestrator names should be nouns associated with the encapsulated volatility (AccountManager, MarketManager — good; BillingManager — bad, the gerund "billing" signals functional grouping around an activity). Strategy/engine names should indicate the volatile activity (SearchEngine, TransformationEngine — good; AccountEngine — bad, no indication of what activity varies). If you struggle to name the component after a volatility axis, it may not encapsulate one.
4. Change Blast Radius
For a plausible change scenario (new provider, new format, new rule), trace how many modules would need to be modified. If the change leaks across boundaries, the decomposition is functional, not volatility-based.
Volatility should decrease downward. In a layered system, higher layers (clients, UI) should be the most volatile, and lower layers (data access, infrastructure) should be the least volatile. "The components in the lower layers have more items that depend on them. If the components you depend upon the most are also the most volatile, your system will implode." If high volatility lives deep in the stack, the blast radius of change is maximized.
Check symmetry. All good architectures are symmetric — you should see the same calling patterns across similar modules. If three of four workflows publish events but the fourth doesn't, or only one module has a particular coupling pattern, that asymmetry is a red flag for functional decomposition or a missed volatility axis. Symmetry can also be broken by the presence of something, not just its absence.
5. Interface Stability
Is the interface between modules stable under the changes the module encapsulates? The receptacle doesn't change when you switch from grid to solar. If the interface must change when the encapsulated volatility changes, the abstraction is leaking.
Expose atomic business verbs, not implementation operations. Löwy's key interface design principle: interfaces should expose indivisible business-level operations (credit, debit, transfer) rather than CRUD or implementation details. "Atomic business verbs are practically immutable because they relate strongly to the nature of the business which hardly ever changes." An interface that exposes OpenPort(), ClosePort(), AdjustBeam() alongside ReadCode() is mixing communication volatility with reading volatility — two axes jammed behind one interface.
Good interfaces are reusable; implementations never are. Löwy's "tool-hand" analogy: a stone axe and a computer mouse have nothing in common internally, but both reuse the same hand interface. If an interface can only be used by one consumer, it may be shaped around the implementation (functional) rather than around a stable contract (volatility-based). Well-designed contracts are logically consistent, cohesive, and independent.
6. Reuse Signal
Volatility-based building blocks are reusable because they encapsulate one axis of change. If a module can only be used in one context, it may be encapsulating functionality rather than volatility.
Löwy observes that reuse increases downward through layers: infrastructure and data-access components should be highly reusable across contexts, business-logic orchestrators are reusable across multiple clients, and clients/UI are rarely reusable. If a lower-layer component is locked to a single consumer, the boundary likely tracks functionality rather than a genuine axis of change.
7. The Almost-Expendable Test
Löwy's litmus test for correct decomposition: when a change request arrives, the response should be contemplative — you think through how to adapt. If a module is expensive to change, it's too big (functional decomposition has coupled unrelated concerns). If a module is expendable (trivially thrown away), it's an unnecessary boundary. If a module is almost expendable — it encapsulates just enough to contain one axis of change, and replacing it is straightforward but not trivial — the decomposition is correct.
Self-Verification Checklist
After completing all steps, audit your own output against this checklist. If any item fires, revise before presenting to the user.
Dismissed Findings
- Findings you talked yourself out of ("However, this is a reasonable grouping..." / "acceptable for now")
- Functional boundaries rationalized as volatility boundaries without naming the concrete axis of change
- "Low blast radius" used as a synonym for "ignore"
- Change scenarios you didn't actually trace through the code
- Domain decomposition dressed up as volatility decomposition
Premature Closure Phrases
These phrase shapes mean you stopped one step early:
- "This boundary groups related functionality but could also be seen as encapsulating volatility" — name the volatility or it's functional decomposition. "Could be seen as" is not an axis of change.
- "The interface would only need minor changes" — minor interface changes are still leaking. The receptacle doesn't change at all.
- "This module is only used in one place, but that's fine for now" — single-use is the reuse signal firing. Investigate.
- "The boundary follows the framework's conventions" — framework conventions are functional decomposition by default. Convention is not volatility analysis.
- "This could theoretically change independently" — theoretical independence without a concrete change scenario is wishful thinking.
- "Out of scope for this PR" / "pre-existing" — process judgment, not a volatility judgment. Defer with an issue link or fix it.
- "The module encapsulates [domain entity]" — domain entities are not volatility axes. What about the entity changes? Name the specific volatility or it's domain decomposition.
- "This is variable, so we should encapsulate it" — variable is not volatile. Can you state the risk in terms of likelihood and effect?
Output Format
-
Boundaries examined — List each module boundary or decomposition decision reviewed.
-
Volatility map — For each boundary: what volatility it encapsulates (or fails to), classified as sequence or activity volatility where applicable.
-
Findings — Boundaries that track functionality rather than volatility, with blast-radius analysis. Include symmetry violations and layering inversions.
-
Simplifications — Concrete restructuring to align boundaries with axes of change.
-
Self-verification result — Output of the Self-Verification Checklist on this evaluation, including the premature closure phrase check.
-
Actions — One entry per finding, formatted for downstream consumption. Each entry starts with a short bolded finding label (≤8 words) naming what is wrong, then dispositions it as Fix in this PR or Defer (with issue reference). Every finding must appear here — including those labeled "pre-existing" or "orthogonal". A finding that never reaches this section has been dismissed, not deferred.
Example: **useViewport encapsulates ghost concern** — Fix in this PR: delete the hook, let FitAddon measure per-tile.
No findings → "No actions." Findings without actions = incomplete review.
Relationship to /hickey
This workflow and /hickey are complementary lenses. Hickey asks "are independent concerns interleaved?" Löwy asks "do boundaries encapsulate axes of change?" Run both on architectural decisions for full coverage.
The two lenses can produce conflicting recommendations. Löwy may say "merge these — shared volatility is duplicated across both" while Hickey says "keep them separate — a mode flag would complect configuration with implementation." Neither lens is wrong; they're optimizing for different things.
The resolution pattern: unify the volatile axis without complecting the strategies. Typically this means a wrapper or shared module that encapsulates the volatile part (satisfying Löwy) while the distinct strategies remain private and uncomplected (satisfying Hickey). If merging for blast-radius reduction requires a mode flag, conditional branching, or type-switching — that's complecting. Find the layer where unification is mechanical (a shared function, a common interface, a single config source) rather than conditional.