Volume 3: Human-System Collaboration

Pattern 11: Cascading Updates

Part II: Interaction Patterns - Relationship Patterns


Opening Scenario: The Customer That Changed Everything

Tom was building an order management system for a regional distributor. The sales team would create orders by filling out a form with customer information, products, quantities, and shipping details.

One morning, Lisa from sales called Tom, frustrated.

"Tom, I just changed the customer from Johnson Hardware to Martinez Construction, and now I have to manually update everything. The shipping address is still Johnson's warehouse. The payment terms are still Net 30 instead of Martinez's Net 15. The sales tax is still calculating for Pennsylvania instead of Ohio. Why doesn't the system update all this automatically?"

Tom looked at his code:

// Bad: Fields are independent
function handleCustomerChange(newCustomer) {
  // Just update the customer field
  form.customer = newCustomer;

  // User has to manually update everything else!
}

"I didn't think about that," Tom admitted. "Let me fix it."

He added some logic:

// Better: Update some related fields
function handleCustomerChange(newCustomer) {
  form.customer = newCustomer;

  // Update shipping address
  form.shippingAddress = newCustomer.defaultShippingAddress;

  // Update payment terms
  form.paymentTerms = newCustomer.defaultPaymentTerms;
}

Lisa tested it. "Better! But the tax rate didn't update. And the salesperson field still shows my name - Martinez is actually Jennifer's account, not mine."

Tom realized the problem was deeper than he thought. When you change a customer, a cascade of updates should ripple through the form:

Customer changes →
  ├─ Shipping address updates (customer's default)
  ├─ Billing address updates (customer's billing address)
  ├─ Payment terms update (customer's terms)
  ├─ Sales tax rate updates (based on shipping state)
  ├─ Salesperson updates (account owner)
  ├─ Price list updates (customer's pricing tier)
  ├─ Available products update (customer's restrictions)
  ├─ Discount percentage updates (customer's negotiated discount)
  ├─ Currency updates (customer's preferred currency)
  └─ All amounts recalculate (in new currency with new prices)

But it wasn't just about updating fields - it was about doing it intelligently:

Question 1: What if the user already modified the shipping address? - Don't overwrite their work - But maybe suggest the customer's default address - Track which fields the user has explicitly changed

Question 2: What if the new customer requires different products? - Clear incompatible line items - Show warning: "Johnson sells fasteners; Martinez buys lumber" - Offer to save current items and start fresh

Question 3: What order should updates happen in? - Shipping address must update before tax rate - Price list must update before line item prices - Currency must update before any amount calculations - Dependencies matter!

Question 4: Should updates be silent or visible? - User needs to see what changed - But not be overwhelmed with notifications - Highlight changed fields briefly


Six months later, Tom demonstrated the new system. Lisa selected a different customer:

[Customer changed from Johnson Hardware to Martinez Construction]

┌─────────────────────────────────────────────────────┐
│ The following fields were updated:                  │
│                                                      │
│ ✓ Shipping Address → 789 Industrial Blvd, Toledo, OH│
│ ✓ Payment Terms → Net 15                            │
│ ✓ Sales Tax → 5.75% (Ohio)                          │
│ ✓ Salesperson → Jennifer Martinez                   │
│ ✓ Price List → Construction (Tier 2)                │
│                                                      │
│ ⚠ Note: 3 line items are not available for this     │
│   customer. They've been marked for review.         │
│                                                      │
│ [Keep Changes]  [Undo All]  [Review Item by Item]   │
└─────────────────────────────────────────────────────┘

The system cascaded updates intelligently: - Updated all dependent fields in correct order - Preserved user edits where appropriate - Flagged incompatible data - Showed what changed and why - Gave user control to accept or reject

Order creation time dropped 40%. Data consistency improved dramatically. And Lisa stopped calling Tom with complaints.

Context

Cascading Updates applies when:

Fields are logically related: Changing one field affects others (customer → address → tax rate)

Consistency matters: Related fields must stay synchronized

User workflow is sequential: Users fill form top to bottom

Domain relationships exist: Business rules define dependencies

Manual updates are error-prone: Easy to forget to update all related fields

Context changes meaning: Changing one field changes what's valid in others

User time is valuable: Automation saves manual work

Problem Statement

Most forms treat fields as independent, forcing users to manually update related fields:

No automatic updates:

// Bad: User changes state, tax rate doesn't update
function handleStateChange(newState) {
  form.state = newState;
  // Tax rate still shows old state's rate
  // User must manually update tax rate
}

Blind overwrites:

// Bad: Overwrites user's explicit choices
function handleCustomerChange(customer) {
  form.shippingAddress = customer.defaultAddress;
  // But what if user already entered a different address?
  // Their work is lost!
}

Wrong update order:

// Bad: Updates in wrong sequence
function handleCustomerChange(customer) {
  calculateTax();  // ← Uses old shipping address
  updateShippingAddress(customer);  // ← Now address is updated
  // Tax was calculated with wrong address!
}

Silent failures:

// Bad: No feedback about what changed
function handleProductChange(product) {
  form.price = product.price;
  form.taxable = product.taxable;
  form.shippingWeight = product.weight;
  // User has no idea these fields changed
}

No conflict handling:

// Bad: Incompatible changes allowed
function handleCurrencyChange(newCurrency) {
  form.currency = newCurrency;
  // But amounts are still in old currency
  // Prices may not be available in new currency
  // No validation of compatibility
}

We need intelligent cascading that updates related fields in the right order, preserves user intent, and communicates changes clearly.

Forces

Automation vs Control

  • Users want automatic updates
  • But also want control over their data
  • Balance convenience with autonomy

Completeness vs Performance

  • Update all dependent fields thoroughly
  • But updates can be slow
  • Balance completeness with responsiveness

Transparency vs Clutter

  • Users need to know what changed
  • But too many notifications overwhelm
  • Balance awareness with simplicity

Preservation vs Freshness

  • Preserve user's explicit edits
  • But update stale system-generated values
  • Track which is which

Dependency Complexity

  • Deep dependency chains are powerful
  • But can be hard to understand/maintain
  • Balance sophistication with maintainability

Solution

Implement intelligent cascade engine that tracks field relationships, determines update order through dependency graphs, preserves user edits, and communicates changes transparently with user control.

The pattern has five key elements:

1. Dependency Graph

Define relationships between fields:

class CascadeDependencyGraph {
  constructor() {
    this.dependencies = new Map();
    this.fieldMetadata = new Map();
  }

  define(field, config) {
    this.dependencies.set(field, {
      triggers: config.triggers || [],  // Fields that trigger updates to this field
      updates: config.updates || [],    // Fields this field triggers updates for
      updateFn: config.updateFn,        // Function to calculate new value
      priority: config.priority || 0,   // Update order priority
      condition: config.condition       // Optional: only update if condition met
    });

    this.fieldMetadata.set(field, {
      userModified: false,
      lastUpdatedBy: null,  // 'user' or 'cascade'
      lastUpdatedAt: null
    });
  }

  // Example: Define customer-related cascades
  defineCustomerCascades() {
    this.define('customer', {
      updates: [
        'shippingAddress',
        'billingAddress', 
        'paymentTerms',
        'salesperson',
        'priceList',
        'currency'
      ],
      priority: 1  // Highest priority - update first
    });

    this.define('shippingAddress', {
      triggers: ['customer'],
      updates: ['taxRate', 'shippingMethod'],
      updateFn: (customer) => customer.defaultShippingAddress,
      condition: (metadata) => !metadata.userModified,  // Don't overwrite user edits
      priority: 2
    });

    this.define('taxRate', {
      triggers: ['shippingAddress', 'customer'],
      updateFn: (form) => this.calculateTaxRate(form.shippingAddress, form.customer),
      priority: 3  // Calculate after address updates
    });

    this.define('currency', {
      triggers: ['customer'],
      updates: ['allAmountFields'],  // Cascade to all currency fields
      updateFn: (customer) => customer.preferredCurrency,
      priority: 2
    });

    this.define('priceList', {
      triggers: ['customer'],
      updates: ['lineItems'],  // Line item prices need to recalculate
      updateFn: (customer) => customer.pricingTier,
      priority: 2
    });
  }

  getUpdateOrder(changedField) {
    // Build list of fields that need updating
    const toUpdate = new Set();
    const visited = new Set();

    const collectDependents = (field) => {
      if (visited.has(field)) return;
      visited.add(field);

      const config = this.dependencies.get(field);
      if (config && config.updates) {
        config.updates.forEach(dependent => {
          toUpdate.add(dependent);
          collectDependents(dependent);  // Recursively get dependents
        });
      }
    };

    collectDependents(changedField);

    // Sort by priority (higher priority first)
    return Array.from(toUpdate).sort((a, b) => {
      const priorityA = this.dependencies.get(a)?.priority || 0;
      const priorityB = this.dependencies.get(b)?.priority || 0;
      return priorityB - priorityA;
    });
  }
}

2. Cascade Engine

Execute updates intelligently:

class CascadeEngine {
  constructor(dependencyGraph) {
    this.graph = dependencyGraph;
    this.pendingUpdates = [];
    this.updateLog = [];
  }

  async cascade(changedField, newValue, form, context) {
    // Mark field as user-modified
    this.graph.fieldMetadata.get(changedField).userModified = true;
    this.graph.fieldMetadata.get(changedField).lastUpdatedBy = 'user';
    this.graph.fieldMetadata.get(changedField).lastUpdatedAt = new Date();

    // Get fields that need updating
    const fieldsToUpdate = this.graph.getUpdateOrder(changedField);

    // Execute updates in order
    const updates = [];

    for (const field of fieldsToUpdate) {
      const update = await this.updateField(field, form, context);

      if (update) {
        updates.push(update);
      }
    }

    // Return all updates made
    return {
      changedField,
      newValue,
      cascadedUpdates: updates,
      timestamp: new Date()
    };
  }

  async updateField(field, form, context) {
    const config = this.graph.dependencies.get(field);
    const metadata = this.graph.fieldMetadata.get(field);

    // Check condition - should we update this field?
    if (config.condition && !config.condition(metadata, form, context)) {
      return null;  // Skip update
    }

    // Calculate new value
    const oldValue = form[field];
    const newValue = await config.updateFn(form, context);

    // No change needed
    if (oldValue === newValue) {
      return null;
    }

    // Check for conflicts
    if (metadata.userModified && !context.forceOverwrite) {
      // User has explicitly set this - suggest instead of overwrite
      return {
        field,
        oldValue,
        newValue,
        action: 'suggest',
        reason: 'User has modified this field'
      };
    }

    // Update the field
    form[field] = newValue;

    // Update metadata
    metadata.lastUpdatedBy = 'cascade';
    metadata.lastUpdatedAt = new Date();

    // Log the update
    return {
      field,
      oldValue,
      newValue,
      action: 'updated',
      reason: `Based on ${config.triggers.join(', ')}`
    };
  }

  getUpdateSummary(cascadeResult) {
    const summary = {
      totalUpdates: cascadeResult.cascadedUpdates.length,
      updated: cascadeResult.cascadedUpdates.filter(u => u.action === 'updated'),
      suggested: cascadeResult.cascadedUpdates.filter(u => u.action === 'suggest'),
      skipped: cascadeResult.cascadedUpdates.filter(u => u.action === 'skip')
    };

    return summary;
  }
}

3. User Edit Tracking

Distinguish user edits from cascade updates:

class EditTracker {
  constructor() {
    this.editHistory = new Map();
  }

  recordUserEdit(field, value, timestamp = new Date()) {
    if (!this.editHistory.has(field)) {
      this.editHistory.set(field, []);
    }

    this.editHistory.get(field).push({
      value,
      source: 'user',
      timestamp
    });
  }

  recordCascadeUpdate(field, value, trigger, timestamp = new Date()) {
    if (!this.editHistory.has(field)) {
      this.editHistory.set(field, []);
    }

    this.editHistory.get(field).push({
      value,
      source: 'cascade',
      trigger,
      timestamp
    });
  }

  wasUserModified(field) {
    const history = this.editHistory.get(field);
    if (!history || history.length === 0) return false;

    // Check if most recent edit was by user
    const recent = history[history.length - 1];
    return recent.source === 'user';
  }

  getLastUserValue(field) {
    const history = this.editHistory.get(field);
    if (!history) return null;

    // Find most recent user edit
    for (let i = history.length - 1; i >= 0; i--) {
      if (history[i].source === 'user') {
        return history[i].value;
      }
    }

    return null;
  }

  shouldPreserveValue(field, newValue) {
    if (!this.wasUserModified(field)) {
      return false;  // Not user modified, safe to update
    }

    const lastUserValue = this.getLastUserValue(field);

    // If new value same as what user entered, no conflict
    if (newValue === lastUserValue) {
      return false;
    }

    // User modified it to something different - preserve it
    return true;
  }
}

4. Conflict Resolution

Handle cases where cascade conflicts with user edits:

class ConflictResolver {
  constructor() {
    this.strategies = {
      'preserve_user': this.preserveUserEdit,
      'suggest_update': this.suggestUpdate,
      'force_update': this.forceUpdate,
      'ask_user': this.askUser
    };
  }

  async resolve(conflict, strategy = 'suggest_update') {
    const resolver = this.strategies[strategy];
    if (!resolver) {
      throw new Error(`Unknown strategy: ${strategy}`);
    }

    return await resolver.call(this, conflict);
  }

  preserveUserEdit(conflict) {
    // Keep user's value, don't update
    return {
      action: 'preserve',
      field: conflict.field,
      value: conflict.oldValue,
      reason: 'Preserving user edit'
    };
  }

  suggestUpdate(conflict) {
    // Don't update, but show suggestion
    return {
      action: 'suggest',
      field: conflict.field,
      currentValue: conflict.oldValue,
      suggestedValue: conflict.newValue,
      reason: conflict.reason,
      canAccept: true,
      canDismiss: true
    };
  }

  forceUpdate(conflict) {
    // Override user's value
    return {
      action: 'update',
      field: conflict.field,
      value: conflict.newValue,
      reason: 'Required for consistency'
    };
  }

  async askUser(conflict) {
    // Prompt user to choose
    return {
      action: 'prompt',
      field: conflict.field,
      options: [
        { label: `Keep my value: ${conflict.oldValue}`, value: conflict.oldValue },
        { label: `Use suggested: ${conflict.newValue}`, value: conflict.newValue }
      ],
      reason: conflict.reason
    };
  }

  getBatchStrategy(conflicts, context) {
    // For multiple conflicts, determine best strategy

    if (conflicts.length === 0) {
      return 'preserve_user';
    }

    if (conflicts.length === 1) {
      return 'suggest_update';
    }

    if (conflicts.length > 5) {
      // Too many to show individually
      return 'ask_user';  // Batch prompt
    }

    // Critical fields must update
    const critical = conflicts.filter(c => c.critical);
    if (critical.length > 0) {
      return 'force_update';
    }

    return 'suggest_update';
  }
}

5. User Feedback

Communicate changes clearly:

class CascadeFeedback {
  showUpdateSummary(cascadeResult, options = {}) {
    const updates = cascadeResult.cascadedUpdates;

    if (updates.length === 0) {
      return null;  // Nothing to show
    }

    // Group updates by action
    const updated = updates.filter(u => u.action === 'updated');
    const suggested = updates.filter(u => u.action === 'suggest');

    // Build notification
    const notification = {
      title: this.getTitle(cascadeResult.changedField),
      updates: [],
      suggestions: [],
      actions: []
    };

    // Show updates
    updated.forEach(update => {
      notification.updates.push({
        field: this.getFieldLabel(update.field),
        oldValue: update.oldValue,
        newValue: update.newValue,
        icon: '✓'
      });
    });

    // Show suggestions
    suggested.forEach(suggestion => {
      notification.suggestions.push({
        field: this.getFieldLabel(suggestion.field),
        currentValue: suggestion.oldValue,
        suggestedValue: suggestion.newValue,
        reason: suggestion.reason,
        actions: [
          { label: 'Accept', action: () => this.acceptSuggestion(suggestion) },
          { label: 'Dismiss', action: () => this.dismissSuggestion(suggestion) }
        ]
      });
    });

    // Add batch actions
    if (updated.length > 0) {
      notification.actions.push({
        label: 'Undo All',
        action: () => this.undoAll(cascadeResult)
      });
    }

    if (suggested.length > 0) {
      notification.actions.push({
        label: 'Accept All Suggestions',
        action: () => this.acceptAllSuggestions(suggested)
      });
    }

    return notification;
  }

  getTitle(changedField) {
    const labels = {
      'customer': 'Customer Changed',
      'shippingAddress': 'Shipping Address Changed',
      'currency': 'Currency Changed'
    };

    return labels[changedField] || 'Field Updated';
  }

  visuallyHighlightChanges(updates, duration = 3000) {
    updates.forEach(update => {
      const element = document.getElementById(`field-${update.field}`);
      if (element) {
        // Add highlight class
        element.classList.add('cascade-updated');

        // Remove after duration
        setTimeout(() => {
          element.classList.remove('cascade-updated');
        }, duration);
      }
    });
  }

  showInlineNotification(field, message, type = 'info') {
    const fieldElement = document.getElementById(`field-${field}`);
    if (!fieldElement) return;

    const notification = document.createElement('div');
    notification.className = `inline-notification ${type}`;
    notification.innerHTML = `
      <span class="icon">${this.getIcon(type)}</span>
      <span class="message">${message}</span>
    `;

    fieldElement.appendChild(notification);

    // Auto-dismiss after 5 seconds
    setTimeout(() => {
      notification.remove();
    }, 5000);
  }

  getIcon(type) {
    const icons = {
      'info': 'ℹ️',
      'success': '✓',
      'warning': '⚠️',
      'error': '✗'
    };

    return icons[type] || icons['info'];
  }
}

Implementation Details

Complete Cascading System

class IntelligentCascadeSystem {
  constructor() {
    this.graph = new CascadeDependencyGraph();
    this.engine = new CascadeEngine(this.graph);
    this.tracker = new EditTracker();
    this.resolver = new ConflictResolver();
    this.feedback = new CascadeFeedback();

    // Define all cascade relationships
    this.defineRelationships();
  }

  defineRelationships() {
    // Customer cascades
    this.graph.define('customer', {
      updates: ['shippingAddress', 'billingAddress', 'paymentTerms', 
                'salesperson', 'priceList', 'currency'],
      updateFn: async (form, context) => {
        const customer = await this.getCustomer(form.customer);
        return customer;
      },
      priority: 1
    });

    this.graph.define('shippingAddress', {
      triggers: ['customer'],
      updates: ['taxRate', 'shippingMethod'],
      updateFn: (form, context) => {
        const customer = context.customerData;
        return customer.defaultShippingAddress;
      },
      condition: (metadata) => !metadata.userModified,
      priority: 2
    });

    this.graph.define('taxRate', {
      triggers: ['shippingAddress'],
      updateFn: async (form, context) => {
        return await this.calculateTaxRate(form.shippingAddress);
      },
      priority: 3
    });

    // Product cascades
    this.graph.define('product', {
      updates: ['price', 'taxable', 'shippingWeight', 'category'],
      updateFn: async (form, context) => {
        const product = await this.getProduct(form.product);
        return product;
      },
      priority: 1
    });

    this.graph.define('price', {
      triggers: ['product', 'priceList', 'currency'],
      updateFn: async (form, context) => {
        const product = await this.getProduct(form.product);
        const priceList = form.priceList;
        const currency = form.currency;

        return this.getPrice(product, priceList, currency);
      },
      priority: 2
    });

    // Currency cascades
    this.graph.define('currency', {
      triggers: ['customer'],
      updates: ['price', 'subtotal', 'tax', 'total'],
      updateFn: (form, context) => {
        const customer = context.customerData;
        return customer.preferredCurrency;
      },
      priority: 1
    });
  }

  async handleFieldChange(field, newValue, form, context = {}) {
    // Record user edit
    this.tracker.recordUserEdit(field, newValue);

    // Update form
    form[field] = newValue;

    // Fetch any necessary data
    const enrichedContext = await this.enrichContext(field, newValue, context);

    // Execute cascade
    const result = await this.engine.cascade(field, newValue, form, enrichedContext);

    // Handle conflicts
    const conflicts = result.cascadedUpdates.filter(u => u.action === 'suggest');
    if (conflicts.length > 0) {
      const resolution = await this.resolver.resolve(conflicts[0], 'suggest_update');
      // Handle resolution...
    }

    // Show feedback
    const notification = this.feedback.showUpdateSummary(result);
    if (notification) {
      this.displayNotification(notification);
    }

    // Highlight changed fields
    this.feedback.visuallyHighlightChanges(result.cascadedUpdates);

    return result;
  }

  async enrichContext(field, value, baseContext) {
    const context = { ...baseContext };

    // Fetch customer data if customer changed
    if (field === 'customer') {
      context.customerData = await this.getCustomer(value);
    }

    // Fetch product data if product changed
    if (field === 'product') {
      context.productData = await this.getProduct(value);
    }

    return context;
  }

  async getCustomer(customerId) {
    // Fetch from database/API
    return {
      id: customerId,
      name: 'Martinez Construction',
      defaultShippingAddress: {
        street: '789 Industrial Blvd',
        city: 'Toledo',
        state: 'OH',
        zip: '43612'
      },
      defaultBillingAddress: {
        street: '789 Industrial Blvd',
        city: 'Toledo',
        state: 'OH',
        zip: '43612'
      },
      defaultPaymentTerms: 'Net 15',
      pricingTier: 'Construction - Tier 2',
      preferredCurrency: 'USD',
      accountOwner: 'Jennifer Martinez'
    };
  }

  async calculateTaxRate(address) {
    const taxRates = {
      'PA': 0.06,
      'OH': 0.0575,
      'NY': 0.08875
    };

    return taxRates[address.state] || 0;
  }
}

// Usage
const cascade = new IntelligentCascadeSystem();

// User changes customer
const form = {
  customer: 'Johnson Hardware',
  shippingAddress: { city: 'Pittsburgh', state: 'PA' },
  taxRate: 0.06
};

await cascade.handleFieldChange('customer', 'Martinez Construction', form);

// Result:
// form.customer = 'Martinez Construction'
// form.shippingAddress = { city: 'Toledo', state: 'OH' } (cascaded)
// form.taxRate = 0.0575 (cascaded from address)
// form.paymentTerms = 'Net 15' (cascaded)
// ... etc.

Consequences

Benefits

Reduced Manual Work: - Users don't update related fields manually - Saves time and effort - Reduces cognitive load

Improved Consistency: - Related fields stay synchronized - No orphaned or stale data - Business rules enforced automatically

Fewer Errors: - Can't forget to update dependent fields - Correct values calculated automatically - Relationships maintained

Better UX: - Form feels intelligent - Reduces frustration - Faster task completion

Preserved Intent: - User edits protected - System updates when appropriate - Smart conflict resolution

Liabilities

Complexity: - Dependency graphs can be intricate - Circular dependencies must be avoided - Testing all combinations is challenging

Performance: - Deep cascades can be slow - May need async updates - Progress indicators helpful

User Confusion: - "Why did this field change?" - Need clear communication - Transparency essential

Unintended Consequences: - Cascade may update unexpected fields - User may not want updates - Need undo/control mechanisms

Maintenance: - Relationship rules change - Dependencies evolve - Documentation critical

Domain Examples

E-commerce: Product Selection Cascade

// Product selection cascades to category, price, inventory
defineProductCascades() {
  this.cascade('product', {
    updates: [
      {
        field: 'category',
        fn: (product) => product.category
      },
      {
        field: 'price',
        fn: (product, form) => this.getPrice(product, form.priceList),
        condition: (meta) => !meta.userModified  // Preserve negotiated prices
      },
      {
        field: 'inventory',
        fn: async (product) => await this.checkInventory(product.sku),
        display: 'inline'  // Show inline, not in notification
      },
      {
        field: 'shippingWeight',
        fn: (product) => product.weight
      }
    ]
  });
}

Healthcare: Insurance Selection Cascade

// Insurance selection cascades to coverage, copay, authorization rules
defineInsuranceCascades() {
  this.cascade('insurancePlan', {
    updates: [
      {
        field: 'copayAmount',
        fn: (plan, form) => plan.getCopay(form.visitType)
      },
      {
        field: 'requiresAuthorization',
        fn: (plan, form) => plan.requiresAuth(form.procedure)
      },
      {
        field: 'coveragePercentage',
        fn: (plan) => plan.coveragePercent
      },
      {
        field: 'deductibleRemaining',
        fn: async (plan, form) => 
          await this.getDeductibleBalance(form.patient, plan)
      }
    ],
    onComplete: (updates) => {
      // Calculate patient responsibility
      this.calculatePatientResponsibility(updates);
    }
  });
}

Real Estate: Property Type Cascade

// Property type cascades to available features, required fields
definePropertyTypeCascades() {
  this.cascade('propertyType', {
    updates: [
      {
        field: 'availableFeatures',
        fn: (type) => this.getFeaturesForType(type),
        action: 'replace_list'
      },
      {
        field: 'requiredDisclosures',
        fn: (type, form) => this.getRequiredDisclosures(type, form.state),
        action: 'show_checklist'
      },
      {
        field: 'squareFootage',
        fn: (type) => null,  // Clear value
        condition: (meta, form, type) => 
          type === 'land' && form.squareFootage  // Land doesn't have sq ft
      }
    ],
    validation: (form) => {
      // Ensure property type compatible with listing type
      if (form.listingType === 'residential' && 
          !['house', 'condo', 'townhouse'].includes(form.propertyType)) {
        return {
          valid: false,
          message: 'Property type must be residential'
        };
      }
      return { valid: true };
    }
  });
}

Prerequisites: - Volume 3, Pattern 7: Calculated Dependencies (mathematical cascades) - Volume 3, Pattern 9: Contextual Constraints (what's valid changes)

Synergies: - Volume 3, Pattern 8: Intelligent Defaults (cascades are smart defaults) - Volume 3, Pattern 10: Semantic Suggestions (suggest during cascade) - Volume 3, Pattern 14: Cross-Field Validation (validate after cascade)

Conflicts: - User autonomy (cascades can feel too automatic) - Audit requirements (must track all changes)

Alternatives: - Manual updates (let user update everything) - Wizard flow (step-by-step with explicit propagation) - Refresh button (user triggers cascade explicitly)

Known Uses

Salesforce: Changing account cascades to contacts, opportunities, and cases

QuickBooks: Changing customer updates default terms, tax settings, and pricing

E-commerce Platforms (Shopify, WooCommerce): Product selection cascades to price, inventory, shipping

Travel Booking (Expedia, Kayak): Changing destination cascades to hotel options, car rentals, activities

CRM Systems: Contact changes cascade to related opportunities, cases, and activities

ERP Systems (SAP, Oracle): Part number selection cascades to specifications, pricing, and inventory


Further Reading

Academic Foundations

  • Reactive Programming: Bainomugisha, E., et al. (2013). "A Survey on Reactive Programming." ACM Computing Surveys 45(4). https://dl.acm.org/doi/10.1145/2501654.2501666
  • Data Flow Programming: Johnston, W.M., Hanna, J.R.P., & Millar, R.J. (2004). "Advances in Dataflow Programming Languages." ACM Computing Surveys 36(1): 1-34.
  • Dependency Management: Mens, T., et al. (2005). "Challenges in Software Evolution." ICSM '05.

Practical Implementation

Standards & Patterns

  • Pattern 12: Calculated Dependencies - Cascades drive calculations
  • Pattern 14: Contextual Constraints - Cascades update constraints
  • Pattern 5: Form State Tracking - Track cascade state changes
  • Volume 2, Chapter 1: The Universal Event Log - Audit cascade history
  • Volume 1, Chapter 9: User Experience Design - Cascade as transformation

Tools & Libraries

Implementation Examples