Most developers have encountered design principles like SOLID, DRY, and Separation of Concerns. But have you ever wondered what unifies these principles? Or how to apply them systematically when facing a novel design decision?
The Principle of Independent Variation (PIV) provides a unifying framework that explains why established design principles work and helps you reason through unfamiliar design challenges.
In this article, I'll show you how to use PIV as a practical thought framework for making better architectural decisions.
What is PIV?
PIV can be expressed through three complementary formulations, each offering a different lens for reasoning about design:
Pattern Formulation
Separate elements that vary independently; unify elements that vary dependently.
This formulation focuses on observable patterns: do elements change together or separately? It's most useful when you can observe co-change patterns through version control history.
Causal Formulation
Separate what varies for different causes; unite what varies for the same cause.
This formulation emphasizes why variation occurs—the underlying change drivers. It directly connects to terminology from classical principles: what the Single Responsibility Principle calls a "reason to change" is precisely a "cause" or "change driver" in PIV terms.
Structural Formulation
Separate elements governed by different change drivers into distinct units; unify elements governed by the same change driver within a single unit.
This formulation is the most precise and actionable. It explicitly connects the causal perspective (change drivers) to the structural outcome (how elements are grouped into units like classes, modules, or services). This is what PIV fundamentally prescribes: aligning system decomposition with causal structure.
Why Three Formulations?
These aren't different principles—they're complementary angles on the same insight:
- The pattern formulation is intuitive and easily remembered
- The causal formulation helps identify why elements change
- The structural formulation provides explicit guidance on how to organize code
Use whichever formulation fits your current reasoning context. All three lead to the same architectural decisions.
The core insight: Systems evade complexity growth when their structure aligns with the structure of change forces acting upon them.
Why PIV Matters
When independent concerns are tangled together, changes to one force modifications to the other. This coupling multiplies maintenance costs and increases the risk of unintended side effects.
PIV reveals a fundamental pattern:
- When independent change drivers are separated → cohesion increases, coupling decreases
- When independent change drivers are coupled → cohesion decreases, coupling increases
This isn't just about inheritance vs composition or any single design pattern. PIV provides a systematic method for evaluating any design decision.
The PIV Thought Framework: 5 Steps
Here's how to apply PIV to architectural decisions:
📋 Quick Reference: The 5-Step PIV Framework
- Identify Elements - What code components are involved?
- Identify Change Drivers - Why would this code change?
- Assess Independence - Do drivers vary for different reasons?
- Apply PIV Directives - Separate independent, unify dependent
- Evaluate Options - Analyze coupling and cohesion
Step 1: Identify the Elements
Elements are the atomic code components involved in your design decision: classes, functions, modules, files, services, or packages.
Example: You're designing a payment processing system. Your elements include:
-
CreditCardPaymentclass -
PayPalPaymentclass -
TransactionLoggerclass -
FraudDetectorclass -
NotificationServiceclass
Step 2: Identify the Change Drivers
This is the most critical step. Change drivers (also called "reasons to change" or "concerns") are the external forces that necessitate modifications.
Ask yourself:
- Why would this code need to change?
- Who requests these changes?
- What different concerns are at play?
Change drivers arise from diverse sources:
- Business requirements (new features, competitive pressures)
- Regulatory compliance (legal obligations, security mandates)
- Technology evolution (framework updates, performance optimization)
- Stakeholder needs (different users with distinct requirements)
Example: In the payment system, you identify two change drivers:
-
Payment Method Variety Driver
- New payment methods emerge constantly (cryptocurrency, bank transfers, buy-now-pay-later)
- Each method has unique validation rules and processing logic
- Adding cryptocurrency should not require modifying credit card processing
-
Common Payment Infrastructure Driver
- Cross-cutting concerns: logging, fraud detection, notifications
- These evolve for different reasons: performance, security, observability
- Improving fraud detection should not require modifying each payment method
The key question: Do these drivers vary for the same reason or different reasons?
Step 3: Assess Independence
Determine whether your identified change drivers are independent or dependent.
Change drivers are independent when they:
- Serve different stakeholders (end users vs. database administrators)
- Change at different rates (volatile UI trends vs. stable business rules)
- Respond to different types of requirements (functional vs. performance vs. security)
- Require different expertise (domain knowledge vs. infrastructure knowledge)
- Operate at different abstraction levels (high-level policies vs. low-level mechanisms)
Change drivers are dependent when they:
- Represent a unified concept (always evolve together)
- Respond to the same stakeholder or domain
- Always require coordinated changes
Example: Payment methods and infrastructure are independent:
- Payment methods change for business reasons (new providers, regulations)
- Infrastructure changes for technical reasons (performance, security)
- They serve different stakeholders and require different expertise
Step 4: Apply PIV Directives
Now apply PIV's two core directives:
Directive 1: Isolate Divergent Concerns (for independent drivers)
When change drivers are independent → Separate them into different modules
This achieves low coupling between domains. Changes to one concern stay confined to its module without propagating to unrelated concerns.
Directive 2: Unify by Single Purpose (for dependent drivers)
When change drivers are dependent → Unify them in the same module
This achieves high cohesion within modules. Related changes stay localized in one cohesive unit, making changes straightforward and complete.
Example application:
Since payment methods and infrastructure are independent, we should separate them:
// Payment methods: domain abstraction (varies independently)
interface PaymentMethod {
void validate();
PaymentResult executePayment();
}
class CreditCardPayment implements PaymentMethod {
public void validate() { /* Credit card validation */ }
public PaymentResult executePayment() { /* Credit card processing */ }
}
class PayPalPayment implements PaymentMethod {
public void validate() { /* PayPal validation */ }
public PaymentResult executePayment() { /* PayPal processing */ }
}
// Infrastructure services (vary independently)
interface TransactionLogger {
void log(String details);
}
interface FraudDetector {
boolean analyze(PaymentMethod method);
}
interface NotificationService {
void send(String message);
}
// Orchestration layer: composes independent concerns
class PaymentProcessor {
private final TransactionLogger logger;
private final FraudDetector fraudDetector;
private final NotificationService notifier;
public PaymentProcessor(TransactionLogger logger,
FraudDetector fraudDetector,
NotificationService notifier) {
this.logger = logger;
this.fraudDetector = fraudDetector;
this.notifier = notifier;
}
public void process(PaymentMethod method) {
method.validate();
if (fraudDetector.analyze(method)) {
PaymentResult result = method.executePayment();
logger.log("Payment processed: " + result);
notifier.send("Payment confirmation");
}
}
}
Benefits of separation:
- ✅ Independent variation: Add new payment methods without touching infrastructure
- ✅ High cohesion: Each class has one responsibility
- ✅ Low coupling: Payment methods don't depend on infrastructure details
- ✅ Excellent testability: Test each concern in isolation
Step 5: Evaluate Design Options
Compare competing designs by analyzing how they affect coupling and cohesion:
For each design option, ask:
-
Coupling analysis:
- How many dependencies exist?
- Do changes in one module force changes in another?
- Can independent concerns vary without affecting each other?
-
Cohesion analysis:
- Does each module address a single change driver?
- Is related functionality unified in one location?
- Can domain experts reason about their concern without understanding unrelated concerns?
Example evaluation:
❌ Inheritance approach (couples independent drivers):
abstract class Payment {
// Infrastructure mixed with domain interface
protected void logTransaction(String details) { /* ... */ }
protected void detectFraud() { /* ... */ }
public final void process() {
validate();
detectFraud();
executePayment();
logTransaction("Payment processed");
}
protected abstract void validate();
protected abstract void executePayment();
}
class CreditCardPayment extends Payment {
protected void validate() { /* ... */ }
protected void executePayment() { /* ... */ }
// Inherits all infrastructure methods
}
Problems:
- ❌ Infrastructure changes force recompilation of all payment methods
- ❌ Template method enforces rigid execution sequence
- ❌ Low cohesion: payment logic mixed with infrastructure usage
- ❌ Poor testability: cannot isolate payment logic from infrastructure
✅ Composition approach (separates independent drivers):
- Each class has a single responsibility
- Infrastructure and payment methods vary independently
- Explicit, controllable dependencies
- Easy to test each concern in isolation
Real-World Application: Immutability
Let's apply the framework to another design decision: mutable vs. immutable data structures.
Step 1: Identify Elements
- Data structures representing domain entities (e.g.,
Customer) - Methods that modify state
- Code that accesses data concurrently
Step 2: Identify Change Drivers
Two independent drivers:
State Evolution Driver: Business requirements about what needs to change (updating addresses, incrementing counters)
Concurrent Access Driver: Architectural requirements about how many components access data simultaneously (multiple threads, distributed systems)
Step 3: Assess Independence
These drivers are independent:
- State evolution responds to business logic requirements
- Concurrent access responds to architectural/performance requirements
- They serve different concerns and vary for different reasons
Step 4: Apply PIV Directives
Since the drivers are independent, we should separate them.
❌ Mutable structures (couple the drivers):
class Customer {
private String address;
public void updateAddress(String newAddress) {
synchronized(this) { // Concurrency concern
this.address = newAddress; // Business logic concern
}
}
}
Problem: State evolution code must include synchronization logic. Changing concurrency patterns (adding readers, changing threading model) forces modifications to state evolution methods. Low cohesion: each method addresses multiple concerns simultaneously.
✅ Immutable structures (decouple the drivers):
record Customer(String address) {
public Customer updateAddress(String newAddress) {
return new Customer(newAddress); // Pure business logic
}
}
Benefits:
- State evolution logic is pure business logic—no synchronization needed
- Concurrent access is inherently safe—immutable data needs no locks
- High cohesion: transformation methods focus purely on business semantics
- Changes to concurrency patterns don't affect state transformation code
Step 5: Evaluate
Immutability achieves better independent variation for these change drivers, though it comes with trade-offs (performance cost, memory overhead). PIV doesn't dictate absolute rules—it reveals the coupling/cohesion implications, letting you balance them against other concerns.
The Deeper Pattern: Coupling ↔ Cohesion Reciprocity
PIV reveals a fundamental reciprocal relationship:
Reducing coupling between independent drivers → Increases cohesion within modules
Increasing coupling between independent drivers → Decreases cohesion within modules
This isn't coincidence—it's intrinsic to PIV's directives. When you separate independent concerns:
- Each module can focus on one responsibility (high cohesion)
- Modules don't force changes on each other (low coupling)
When you couple independent concerns:
- Each module mixes multiple responsibilities (low cohesion)
- Changes ripple across module boundaries (high coupling)
When to Use Each Formulation
PIV's three formulations serve different reasoning contexts. Choose the lens that best fits your current situation:
Use the Pattern Formulation when:
- You're analyzing version control history and co-change patterns
- You want to observe which elements change together over time
- You're doing empirical analysis of existing codebases
- You need to communicate design concepts in the simplest terms
Ask: "Do these elements change together or separately?"
Use the Causal Formulation when:
- You understand your system's evolution patterns
- You can identify concrete stakeholders and their needs
- You're analyzing why modifications occur
- You're designing for a specific, known context
- You need to connect to terminology from SRP and other principles
Ask: "What are the independent reasons this code will change?"
Use the Structural Formulation when:
- You're making explicit decisions about module boundaries
- You need to determine how to group classes into packages
- You're defining service boundaries in microservices
- You're organizing code into architectural layers
- You want the most precise, actionable guidance
Ask: "Which change drivers govern these elements, and are they independent?"
When Causal Analysis is Unclear
If identifying change drivers proves difficult, fall back to coupling/cohesion analysis:
- Evaluate whether a design choice increases or decreases coupling
- Assess whether related functionality is unified or scattered
- Observe whether modifications to one concern force changes to others
Ask: "Does this design improve cohesion or reduce coupling?"
All three formulations—and the coupling/cohesion perspective—lead to the same architectural conclusions. Use whichever fits your current reasoning context and available information.
Practical Tips for Applying PIV
1. Study Version Control History
The most reliable way to identify change drivers is examining actual change patterns:
# Find files that frequently change together
git log --format= --name-only | sort | uniq -c | sort -rn
# Analyze commit history for a module
git log --oneline path/to/module
Co-change patterns reveal coupled change drivers. Files that change together likely serve the same concern.
2. Examine Issue Trackers
Issues are categorized by type (feature, bug, security, performance). This categorization often reveals distinct change drivers:
- Feature requests → Domain/business drivers
- Performance issues → Infrastructure drivers
- Security vulnerabilities → Cross-cutting security driver
3. Talk to Stakeholders
Different stakeholders represent different change drivers:
- Product managers → Business requirements
- End users → UX/functional requirements
- Operations teams → Performance/reliability requirements
- Security teams → Security requirements
- Compliance officers → Regulatory requirements
4. Look for Linguistic Boundaries
Domain-Driven Design's "ubiquitous language" helps identify domains. When terms have different meanings in different contexts, you've found a domain boundary—and likely distinct change drivers.
5. Consider Future Evolution
Ask: "If X changes, what else must change?"
If changing X forces changes to Y, but they represent different concerns, you've found coupled independent drivers—a PIV violation.
PIV Explains Other Principles
One of PIV's powers is explaining why established principles work:
Single Responsibility Principle (SRP)
"A class should have one reason to change"
PIV generalization: Separate elements that vary for different reasons. SRP is PIV applied at the class level.
Dependency Inversion Principle (DIP)
"High-level policies should not depend on low-level details"
PIV explanation: Business rules and infrastructure technology are independent change drivers. DIP decouples them by introducing abstractions that allow each to vary independently.
Interface Segregation Principle (ISP)
"Clients should not depend on interfaces they don't use"
PIV explanation: A "fat" interface couples multiple independent client needs (change drivers). When one client's needs evolve, all clients are affected. Segregating interfaces decouples these independent drivers.
Open/Closed Principle (OCP)
"Open for extension, closed for modification"
PIV explanation: Abstractions succeed when they capture the stable intersection of multiple change drivers' requirements, while implementations vary independently through extension.
Limitations and Context
PIV is powerful, but not absolute:
PIV Doesn't Dictate Every Decision
Other concerns may sometimes override pure PIV optimization:
- Performance requirements (denormalization for speed)
- Security constraints (defense-in-depth may require duplication)
- Deployment constraints (organizational boundaries)
- Team structure (Conway's Law considerations)
Identifying Change Drivers is Hard
This is the most challenging aspect of applying PIV. It requires:
- Deep understanding of the domain
- Historical analysis of how technology evolved
- Empathy for different stakeholder perspectives
- Recognition that change drivers aren't always obvious
When in doubt, examine version history, issue trackers, and migration guides to identify recurring modification patterns.
Independence Isn't Binary
Change drivers exist on a spectrum from completely independent to completely dependent. Exercise architectural judgment about the degree of independence.
The PIV Decision Framework
When facing any design decision, follow this checklist:
1. Identify change drivers
- What are the reasons this code will change?
- Who requests these changes?
- What different concerns are at play?
2. Check independence
- Do these drivers vary for different reasons?
- Business vs. technical concerns?
- Domain vs. infrastructure?
- Different stakeholders?
3. If independent → Prefer separation
- Create interfaces for each driver
- Use dependency injection
- Keep concerns in separate modules
4. If dependent → Unification is appropriate
- They represent a unified concept
- They always evolve together
- Bring related code into one cohesive unit
5. Evaluate trade-offs
- How does each option affect coupling?
- How does each option affect cohesion?
- Are there overriding performance/security concerns?
- What's the cost of change for each approach?
Example: Applying PIV to Database Normalization
Let's practice the framework one more time.
Step 1: Elements
- Database tables
- Queries that read data
- Queries that write data
Step 2: Change Drivers
- Data Integrity Driver: Business rules about consistency, correctness, preventing anomalies
- Query Performance Driver: Response time requirements, read/write patterns
- Schema Evolution Driver: Adding fields, changing relationships
Step 3: Independence
These drivers are largely independent:
- Data integrity responds to business logic correctness concerns
- Performance responds to user experience and scale concerns
- Schema evolution responds to changing business requirements
Step 4: Apply PIV
This reveals why normalization works (and when it doesn't):
Normalization (high cohesion approach):
- Groups related data by domain concept
- Achieves high cohesion: each table represents one concept
- Data integrity and schema evolution benefit
- Query performance may suffer (joins required)
Denormalization (performance optimization):
- Couples data from multiple concepts for query speed
- Reduces cohesion: tables mix multiple concerns
- Query performance benefits
- Data integrity and schema evolution become harder
PIV guidance: Prefer normalization for the identified drivers (integrity, evolution), but consider denormalization when the performance driver dominates (high-traffic read paths, proven bottlenecks).
CQRS/Event Sourcing (maximum separation):
- Separate write models (optimized for integrity) from read models (optimized for performance)
- Each model varies independently for its driver
- Maximizes independent variation but adds complexity
The Takeaway
PIV provides a systematic thought framework for architectural decisions:
- Identify elements in your design
- Identify change drivers that force modifications
- Assess independence between drivers
- Apply PIV directives: separate independent drivers, unify dependent ones
- Evaluate options by analyzing coupling and cohesion
The core insight is simple but profound:
Structure your system so independent concerns can vary independently
When you achieve this, changes stay localized, parallel development becomes possible, cognitive load decreases, and your system becomes genuinely evolvable.
The next time you face a design decision, pause and ask:
"What are the independent change drivers, and does this design separate them?"
If yes, you're on the right path. If no, PIV shows you where coupling exists and how to reduce it.
Learn More
This article is based on research from:
Loth, Y. (2025). The Principle of Independent Variation. Zenodo.
https://doi.org/10.5281/zenodo.17677316
The PIV paper explores this principle in depth, including:
- The Knowledge Theorem (cohesion ↔ domain knowledge equivalence)
- Formal derivation of SOLID principles from PIV
- Detailed analysis of architectural patterns through the PIV lens
- Critical evaluation of measurement approaches and limitations
What design decisions have you reconsidered through the lens of independent change drivers? How do you identify change drivers in your systems? Share your experiences in the comments!
Top comments (0)