Volume 3: Human-System Collaboration

Pattern 12: Mutual Exclusivity

Part II: Interaction Patterns - Relationship Patterns


Opening Scenario: The Checkbox That Couldn't Decide

Karen was building a shipping form for an e-commerce site. She added two options for the shipping address:

<input type="checkbox" name="useShipping"> 
  Use billing address for shipping

<input type="checkbox" name="enterShipping"> 
  Enter different shipping address

During testing, a user clicked both checkboxes. The system didn't know what to do:

✓ Use billing address for shipping
✓ Enter different shipping address

[Shipping address fields shown but ignored because 
 "use billing" is also checked]

The order was submitted with billing address, but the user had entered a different shipping address. The package went to the wrong place.

Karen added validation:

// Bad: Post-submission validation
if (form.useShipping && form.enterShipping) {
  return "Error: Select only one shipping option";
}

But users still clicked both checkboxes, then got an error. Frustrating.

Karen tried a different approach - disable conflicting options:

// Better: Disable conflicts
document.getElementById('useShipping').addEventListener('change', function() {
  if (this.checked) {
    document.getElementById('enterShipping').disabled = true;
    document.getElementById('enterShipping').checked = false;
  } else {
    document.getElementById('enterShipping').disabled = false;
  }
});

This worked, but users were confused: "Why did the other checkbox become grayed out? What if I change my mind?"


Six months later, Karen showed me the redesigned form:

Shipping Address:

( ) Use my billing address
    123 Main St, Pittsburgh, PA 15217

( ) Ship to a different address
    [If selected, address fields appear below]

Radio buttons instead of checkboxes. Mutually exclusive by design.

When user selects "Use my billing address": - Billing address is displayed (read-only) - Shipping address fields are hidden - Clear visual indicator of choice

When user selects "Ship to a different address": - Billing address reference is hidden - Shipping address fields appear - User can enter new address

The interface makes mutual exclusivity obvious. You can't select both because the control type prevents it.

But Karen went further. She realized many field relationships in the form were mutually exclusive:

Marital Status: Can't be both "Married" and "Single"
Payment Method: Can't pay by both "Credit Card" and "PayPal"
Employment Status: Can't be both "Employed" and "Unemployed"
Residence Type: Can't both "Own" and "Rent"

Some were obvious (radio buttons for marital status), but others were hidden in the business logic:

Express Shipping with Free Shipping Promo: Can't combine
New Customer Discount with Loyalty Discount: Only one applies
Gift Wrapping with Digital Delivery: Incompatible

The new system made all mutual exclusivities explicit:

class MutualExclusivityEngine {
  constructor() {
    this.exclusivityRules = new Map();
  }

  defineExclusiveGroup(fields, config) {
    // Fields in this group are mutually exclusive
    this.exclusivityRules.set(config.groupName, {
      fields: fields,
      selection: config.selection || 'single',  // single or multiple
      required: config.required || false,
      onConflict: config.onConflict || 'deselect-others'
    });
  }
}

// Define exclusive relationships
engine.defineExclusiveGroup(
  ['useShipping', 'enterShipping'],
  {
    groupName: 'shippingAddressChoice',
    selection: 'single',  // Only one can be selected
    required: true,       // User must choose one
    onConflict: 'deselect-others'
  }
);

engine.defineExclusiveGroup(
  ['expressShipping', 'freeShippingPromo'],
  {
    groupName: 'shippingMethod',
    selection: 'single',
    onConflict: 'show-explanation',
    explanation: 'Free shipping promotion cannot be combined with express shipping'
  }
);

Order errors dropped to nearly zero. Customer service calls about wrong shipping addresses disappeared. And users found the interface intuitive and clear.

Context

Mutual Exclusivity applies when:

Logical impossibility: Two things cannot both be true (married AND single)

Business rules: Policies prevent combinations (two discounts can't stack)

System constraints: Technical limitations (can't ship physical AND digital at once)

Resource conflicts: Can't allocate same resource to multiple uses

Choice architecture: User must pick one option from a set

State transitions: Moving from one state excludes previous state

Clarity needed: Users confused about valid combinations

Problem Statement

Most forms fail to make mutual exclusivity clear, leading to errors and confusion:

Allow impossible combinations:

<!-- Bad: Both checkboxes can be checked -->
<input type="checkbox" name="married"> Married
<input type="checkbox" name="single"> Single

<!-- User checks both, system doesn't know what to do -->

Late-stage validation:

// Bad: Discover conflict at submit
function validateMaritalStatus(form) {
  if (form.married && form.single) {
    return "Error: You cannot be both married and single";
  }
}

// User already invested time filling out form
// Frustrating to get error at the end

Unclear conflict resolution:

// Bad: Silent override
if (form.newCustomerDiscount && form.loyaltyDiscount) {
  form.newCustomerDiscount = false;  // Silently removed
}

// User doesn't know why discount disappeared

Poor control selection:

<!-- Bad: Checkboxes for exclusive choices -->
<input type="checkbox" name="shipping_standard"> Standard
<input type="checkbox" name="shipping_express"> Express
<input type="checkbox" name="shipping_overnight"> Overnight

<!-- Should be radio buttons! -->

Hidden business rules:

// Bad: Conflict enforced in backend
if (order.giftWrap && order.digitalDelivery) {
  throw new Error("Cannot gift wrap digital products");
}

// User only discovers this at checkout
// Should be prevented in UI

We need interfaces that make mutual exclusivity obvious, prevent conflicts proactively, and explain constraints clearly.

Forces

Prevention vs Correction

  • Better to prevent conflicts than fix them
  • But prevention can feel restrictive
  • Balance guardrails with flexibility

Obviousness vs Simplicity

  • Explicit exclusivity (radio buttons) is clear
  • But can make UI cluttered
  • Balance clarity with clean design

Automatic vs Manual Resolution

  • System can auto-resolve conflicts
  • But users want control
  • Balance automation with transparency

Rigidity vs Context

  • Some exclusivities are absolute
  • Others depend on context
  • Need flexibility for edge cases

Education vs Enforcement

  • Users can learn rules over time
  • But enforcement prevents errors
  • Balance teaching with protection

Solution

Make mutually exclusive relationships explicit through appropriate controls, clear visual design, proactive conflict prevention, and transparent explanation of business rules - preventing impossible states rather than correcting them after the fact.

The pattern has four key strategies:

1. Control-Level Exclusivity

Use form controls that enforce exclusivity by design:

class ExclusiveControls {
  // Radio buttons for single selection from exclusive options
  renderRadioGroup(options, config) {
    const groupName = config.name;

    return `
      <fieldset>
        <legend>${config.label}</legend>
        ${options.map(opt => `
          <label>
            <input 
              type="radio" 
              name="${groupName}" 
              value="${opt.value}"
              ${opt.value === config.default ? 'checked' : ''}
              ${opt.disabled ? 'disabled' : ''}
            >
            ${opt.label}
            ${opt.description ? `<span class="hint">${opt.description}</span>` : ''}
          </label>
        `).join('\n')}
      </fieldset>
    `;
  }

  // Example: Shipping address choice
  renderShippingChoice() {
    return this.renderRadioGroup([
      {
        value: 'billing',
        label: 'Use my billing address',
        description: '123 Main St, Pittsburgh, PA 15217'
      },
      {
        value: 'different',
        label: 'Ship to a different address',
        description: 'Enter address below'
      }
    ], {
      name: 'shippingChoice',
      label: 'Shipping Address',
      default: 'billing'
    });
  }

  // Toggle switch for binary exclusivity
  renderToggle(config) {
    return `
      <div class="toggle-switch">
        <label>
          <input 
            type="checkbox" 
            name="${config.name}"
            ${config.default ? 'checked' : ''}
          >
          <span class="toggle-label-off">${config.labelOff}</span>
          <span class="toggle-label-on">${config.labelOn}</span>
        </label>
      </div>
    `;
  }

  // Example: Employment status toggle
  renderEmploymentToggle() {
    return this.renderToggle({
      name: 'employed',
      labelOff: 'Unemployed',
      labelOn: 'Employed',
      default: true
    });
  }

  // Segmented control for exclusive segments
  renderSegmentedControl(segments, config) {
    return `
      <div class="segmented-control" role="radiogroup" aria-label="${config.label}">
        ${segments.map(seg => `
          <button
            type="button"
            role="radio"
            aria-checked="${seg.value === config.selected ? 'true' : 'false'}"
            data-value="${seg.value}"
            class="${seg.value === config.selected ? 'selected' : ''}"
            onclick="selectSegment('${config.name}', '${seg.value}')"
          >
            ${seg.icon ? `<span class="icon">${seg.icon}</span>` : ''}
            ${seg.label}
          </button>
        `).join('\n')}
      </div>
    `;
  }

  // Example: Shipping speed
  renderShippingSpeed() {
    return this.renderSegmentedControl([
      { value: 'standard', label: 'Standard', icon: '📦' },
      { value: 'express', label: 'Express', icon: '⚡' },
      { value: 'overnight', label: 'Overnight', icon: '🚀' }
    ], {
      name: 'shippingSpeed',
      label: 'Shipping Speed',
      selected: 'standard'
    });
  }
}

2. Dynamic Exclusivity

Enforce exclusivity when controls can't:

class DynamicExclusivityEnforcer {
  constructor() {
    this.exclusivityGroups = new Map();
  }

  defineGroup(groupId, fields, strategy = 'deselect-others') {
    this.exclusivityGroups.set(groupId, {
      fields: new Set(fields),
      strategy: strategy,
      activeField: null
    });

    // Attach change listeners
    fields.forEach(field => {
      this.attachListener(groupId, field);
    });
  }

  attachListener(groupId, fieldName) {
    const element = document.getElementById(fieldName);
    if (!element) return;

    element.addEventListener('change', (e) => {
      if (e.target.checked || e.target.value) {
        this.handleSelection(groupId, fieldName);
      }
    });
  }

  handleSelection(groupId, selectedField) {
    const group = this.exclusivityGroups.get(groupId);
    if (!group) return;

    switch(group.strategy) {
      case 'deselect-others':
        this.deselectOthers(group, selectedField);
        break;

      case 'disable-others':
        this.disableOthers(group, selectedField);
        break;

      case 'hide-others':
        this.hideOthers(group, selectedField);
        break;

      case 'prompt-user':
        this.promptUser(group, selectedField);
        break;
    }

    group.activeField = selectedField;
  }

  deselectOthers(group, selectedField) {
    group.fields.forEach(field => {
      if (field !== selectedField) {
        const element = document.getElementById(field);
        if (element) {
          if (element.type === 'checkbox') {
            element.checked = false;
          } else {
            element.value = '';
          }

          // Trigger dependent changes
          element.dispatchEvent(new Event('change'));
        }
      }
    });
  }

  disableOthers(group, selectedField) {
    group.fields.forEach(field => {
      if (field !== selectedField) {
        const element = document.getElementById(field);
        if (element) {
          element.disabled = true;
          this.showDisabledReason(element, 
            `Disabled because ${selectedField} is selected`);
        }
      }
    });
  }

  hideOthers(group, selectedField) {
    group.fields.forEach(field => {
      if (field !== selectedField) {
        const container = this.getFieldContainer(field);
        if (container) {
          container.style.display = 'none';
        }
      }
    });
  }

  async promptUser(group, selectedField) {
    if (group.activeField && group.activeField !== selectedField) {
      const confirmed = await this.showConfirmation(
        `Selecting ${selectedField} will deselect ${group.activeField}. Continue?`
      );

      if (confirmed) {
        this.deselectOthers(group, selectedField);
      } else {
        // Revert selection
        const element = document.getElementById(selectedField);
        if (element) {
          element.checked = false;
        }
      }
    }
  }

  showDisabledReason(element, reason) {
    const hint = document.createElement('span');
    hint.className = 'disabled-hint';
    hint.textContent = reason;

    const container = element.parentElement;
    container.appendChild(hint);
  }
}

// Usage
const enforcer = new DynamicExclusivityEnforcer();

// Discount codes are mutually exclusive
enforcer.defineGroup('discounts', [
  'newCustomerDiscount',
  'loyaltyDiscount',
  'seasonalPromo'
], 'deselect-others');

// Shipping options are exclusive with explanations
enforcer.defineGroup('shippingOptions', [
  'freeShipping',
  'expressShipping',
  'pickupInStore'
], 'prompt-user');

3. Business Rule Exclusivity

Enforce domain-specific exclusive relationships:

class BusinessRuleExclusivity {
  constructor() {
    this.rules = [];
  }

  addRule(rule) {
    this.rules.push({
      name: rule.name,
      fields: rule.fields,
      condition: rule.condition,
      message: rule.message,
      resolution: rule.resolution || 'prevent'
    });
  }

  validate(formData) {
    const violations = [];

    this.rules.forEach(rule => {
      if (rule.condition(formData)) {
        violations.push({
          rule: rule.name,
          fields: rule.fields,
          message: rule.message,
          resolution: rule.resolution
        });
      }
    });

    return violations;
  }

  // Define product incompatibilities
  defineProductConflicts() {
    this.addRule({
      name: 'gift-wrap-digital',
      fields: ['giftWrap', 'product'],
      condition: (form) => {
        return form.giftWrap && 
               form.product && 
               form.product.type === 'digital';
      },
      message: 'Gift wrapping is not available for digital products',
      resolution: 'prevent'
    });

    this.addRule({
      name: 'express-free-shipping',
      fields: ['shippingSpeed', 'promoCode'],
      condition: (form) => {
        return form.shippingSpeed === 'express' && 
               form.promoCode === 'FREESHIP';
      },
      message: 'Free shipping promotion only applies to standard shipping',
      resolution: 'suggest-alternative'
    });

    this.addRule({
      name: 'international-overnight',
      fields: ['shippingCountry', 'shippingSpeed'],
      condition: (form) => {
        return form.shippingCountry !== 'US' && 
               form.shippingSpeed === 'overnight';
      },
      message: 'Overnight shipping not available for international orders',
      resolution: 'prevent'
    });
  }

  // Define discount stacking rules
  defineDiscountConflicts() {
    this.addRule({
      name: 'single-discount-only',
      fields: ['discounts'],
      condition: (form) => {
        const activeDiscounts = form.discounts.filter(d => d.active);
        return activeDiscounts.length > 1;
      },
      message: 'Only one discount can be applied per order',
      resolution: 'choose-best'
    });

    this.addRule({
      name: 'new-customer-loyalty-conflict',
      fields: ['newCustomerDiscount', 'loyaltyPoints'],
      condition: (form) => {
        return form.newCustomerDiscount && form.loyaltyPoints > 0;
      },
      message: 'New customer discount cannot be combined with loyalty points',
      resolution: 'prompt-user'
    });
  }

  // Resolve conflicts automatically when possible
  autoResolve(formData, violation) {
    switch(violation.resolution) {
      case 'prevent':
        return this.preventConflict(formData, violation);

      case 'choose-best':
        return this.chooseBestOption(formData, violation);

      case 'suggest-alternative':
        return this.suggestAlternative(formData, violation);

      case 'prompt-user':
        return this.promptUserChoice(formData, violation);
    }
  }

  preventConflict(formData, violation) {
    // Don't allow the conflicting selection
    return {
      action: 'block',
      message: violation.message,
      affectedFields: violation.fields
    };
  }

  chooseBestOption(formData, violation) {
    // Automatically select best value for user
    if (violation.rule === 'single-discount-only') {
      const discounts = formData.discounts.filter(d => d.active);
      const best = discounts.reduce((max, d) => 
        d.amount > max.amount ? d : max
      );

      return {
        action: 'auto-select',
        message: `Applied ${best.name} (highest discount)`,
        selection: best,
        deselected: discounts.filter(d => d !== best)
      };
    }
  }

  suggestAlternative(formData, violation) {
    // Suggest compatible alternative
    if (violation.rule === 'express-free-shipping') {
      return {
        action: 'suggest',
        message: violation.message,
        alternatives: [
          {
            label: 'Use free standard shipping',
            changes: { shippingSpeed: 'standard' }
          },
          {
            label: 'Pay for express shipping',
            changes: { promoCode: null }
          }
        ]
      };
    }
  }
}

4. Visual Communication

Make exclusivity obvious through design:

class ExclusivityVisualizer {
  // Highlight mutually exclusive groups
  highlightGroup(groupId, fields) {
    const container = document.createElement('div');
    container.className = 'exclusive-group';
    container.setAttribute('data-group', groupId);

    // Visual indicator
    const indicator = document.createElement('div');
    indicator.className = 'exclusive-indicator';
    indicator.innerHTML = '⊕ Choose one';
    container.appendChild(indicator);

    // Wrap fields
    fields.forEach(fieldId => {
      const fieldElement = document.getElementById(fieldId);
      const fieldContainer = fieldElement.closest('.field-container');
      container.appendChild(fieldContainer);
    });

    return container;
  }

  // Show conflict with animation
  showConflict(field1, field2, message) {
    // Highlight conflicting fields
    [field1, field2].forEach(fieldId => {
      const element = document.getElementById(fieldId);
      element.classList.add('conflict');

      // Shake animation
      element.classList.add('shake');
      setTimeout(() => element.classList.remove('shake'), 500);
    });

    // Show conflict message
    this.showConflictMessage(field1, field2, message);

    // Remove highlights after delay
    setTimeout(() => {
      [field1, field2].forEach(fieldId => {
        document.getElementById(fieldId).classList.remove('conflict');
      });
    }, 3000);
  }

  showConflictMessage(field1, field2, message) {
    const messageBox = document.createElement('div');
    messageBox.className = 'conflict-message';
    messageBox.innerHTML = `
      <span class="icon">⚠️</span>
      <span class="text">${message}</span>
      <button onclick="this.parentElement.remove()">Dismiss</button>
    `;

    // Position between conflicting fields
    const field1Elem = document.getElementById(field1);
    const field2Elem = document.getElementById(field2);
    const midpoint = (field1Elem.offsetTop + field2Elem.offsetTop) / 2;

    messageBox.style.top = midpoint + 'px';

    document.body.appendChild(messageBox);

    // Auto-dismiss
    setTimeout(() => messageBox.remove(), 5000);
  }

  // Show exclusivity relationships in help
  renderExclusivityDiagram(groups) {
    const diagram = document.createElement('div');
    diagram.className = 'exclusivity-diagram';

    groups.forEach(group => {
      const groupVis = document.createElement('div');
      groupVis.className = 'exclusive-group-vis';

      groupVis.innerHTML = `
        <div class="group-label">${group.name}</div>
        <div class="group-members">
          ${group.fields.map(field => `
            <div class="member">${field.label}</div>
          `).join('<div class="separator">OR</div>')}
        </div>
        <div class="group-rule">${group.rule}</div>
      `;

      diagram.appendChild(groupVis);
    });

    return diagram;
  }
}

Implementation Details

Complete Mutual Exclusivity System

class MutualExclusivitySystem {
  constructor() {
    this.controls = new ExclusiveControls();
    this.enforcer = new DynamicExclusivityEnforcer();
    this.businessRules = new BusinessRuleExclusivity();
    this.visualizer = new ExclusivityVisualizer();
  }

  initialize(formConfig) {
    // Define all exclusive relationships
    this.defineRelationships(formConfig);

    // Set up enforcement
    this.setupEnforcement();

    // Add visual indicators
    this.setupVisuals();
  }

  defineRelationships(config) {
    // Control-level exclusivity (radio buttons, toggles)
    config.radioGroups?.forEach(group => {
      this.controls.renderRadioGroup(group.options, group.config);
    });

    // Dynamic exclusivity (checkboxes, dropdowns)
    config.exclusiveGroups?.forEach(group => {
      this.enforcer.defineGroup(
        group.id,
        group.fields,
        group.strategy
      );
    });

    // Business rule exclusivity
    config.businessRules?.forEach(rule => {
      this.businessRules.addRule(rule);
    });
  }

  setupEnforcement() {
    // Listen for field changes
    document.addEventListener('change', async (e) => {
      const field = e.target.name;
      const formData = this.getFormData();

      // Check for business rule violations
      const violations = this.businessRules.validate(formData);

      if (violations.length > 0) {
        await this.handleViolations(violations, formData);
      }
    });
  }

  async handleViolations(violations, formData) {
    for (const violation of violations) {
      const resolution = await this.businessRules.autoResolve(
        formData,
        violation
      );

      switch(resolution.action) {
        case 'block':
          this.visualizer.showConflict(
            violation.fields[0],
            violation.fields[1],
            resolution.message
          );
          // Revert change
          break;

        case 'auto-select':
          this.applyAutoSelection(resolution);
          this.showNotification(resolution.message, 'info');
          break;

        case 'suggest':
          await this.showAlternatives(resolution);
          break;
      }
    }
  }

  getFormData() {
    const form = document.querySelector('form');
    const data = new FormData(form);
    return Object.fromEntries(data.entries());
  }
}

// Example usage
const system = new MutualExclusivitySystem();

system.initialize({
  radioGroups: [
    {
      options: [
        { value: 'billing', label: 'Use billing address' },
        { value: 'different', label: 'Different address' }
      ],
      config: {
        name: 'shippingChoice',
        label: 'Shipping Address',
        default: 'billing'
      }
    }
  ],

  exclusiveGroups: [
    {
      id: 'discounts',
      fields: ['newCustomer', 'loyalty', 'seasonal'],
      strategy: 'deselect-others'
    }
  ],

  businessRules: [
    {
      name: 'gift-wrap-digital',
      fields: ['giftWrap', 'productType'],
      condition: (form) => form.giftWrap && form.productType === 'digital',
      message: 'Digital products cannot be gift wrapped',
      resolution: 'prevent'
    }
  ]
});

Consequences

Benefits

Prevents Invalid States: - Impossible combinations blocked - Users can't create conflicts - Data integrity maintained

Clear Communication: - Visual design shows exclusivity - Users understand constraints - Less confusion and errors

Better UX: - Appropriate controls for choices - Conflicts prevented, not corrected - Faster task completion

Reduced Support: - Fewer "why can't I..." questions - Self-evident constraints - Clear error messages when needed

Enforced Business Rules: - Policies implemented consistently - Domain knowledge embedded - Compliance automated

Liabilities

Reduced Flexibility: - May prevent valid edge cases - Users can't override rules - Need escape hatches for exceptions

Implementation Complexity: - Multiple enforcement strategies - Business rules can be intricate - Testing all combinations challenging

User Frustration: - Disabled options can confuse - "Why can't I select both?" - Need good explanations

Maintenance: - Business rules change - New exclusivities discovered - Must update all enforcement layers

Performance: - Real-time validation adds overhead - Complex rule checking can be slow - Need optimization for large forms

Domain Examples

E-commerce: Product Configuration

// Product options with mutual exclusivity
defineProductExclusivity() {
  // Size and fit are mutually exclusive
  this.enforcer.defineGroup('size', [
    'size_small',
    'size_medium',
    'size_large',
    'size_xlarge'
  ], 'deselect-others');

  // Color choices exclusive
  this.enforcer.defineGroup('color', [
    'color_red',
    'color_blue',
    'color_green'
  ], 'deselect-others');

  // Shipping method exclusive
  this.enforcer.defineGroup('shipping', [
    'standard_shipping',
    'express_shipping',
    'pickup_in_store'
  ], 'deselect-others');

  // Business rule: Can't gift wrap + digital delivery
  this.businessRules.addRule({
    name: 'gift-wrap-incompatible',
    fields: ['giftWrap', 'deliveryMethod'],
    condition: (form) => 
      form.giftWrap && form.deliveryMethod === 'digital',
    message: 'Gift wrapping not available for digital delivery',
    resolution: 'prevent'
  });
}

Healthcare: Insurance Selection

// Insurance coverage mutual exclusivity
defineInsuranceExclusivity() {
  // Primary vs Secondary coverage
  this.businessRules.addRule({
    name: 'coverage-type',
    fields: ['primaryInsurance', 'secondaryInsurance'],
    condition: (form) => {
      // Same insurer can't be both primary and secondary
      return form.primaryInsurance === form.secondaryInsurance &&
             form.primaryInsurance !== null;
    },
    message: 'Primary and secondary insurance must be different',
    resolution: 'prevent'
  });

  // Coverage period exclusivity
  this.businessRules.addRule({
    name: 'effective-dates',
    fields: ['coverage1', 'coverage2'],
    condition: (form) => {
      // Overlapping coverage periods not allowed
      return this.datesOverlap(
        form.coverage1.start, form.coverage1.end,
        form.coverage2.start, form.coverage2.end
      );
    },
    message: 'Coverage periods cannot overlap',
    resolution: 'prompt-user'
  });
}

Real Estate: Property Features

// Property listing mutual exclusivity
definePropertyExclusivity() {
  // Sale vs Rent exclusive
  this.controls.renderRadioGroup([
    { value: 'sale', label: 'For Sale' },
    { value: 'rent', label: 'For Rent' },
    { value: 'both', label: 'Sale or Rent' }
  ], {
    name: 'listingType',
    label: 'Listing Type'
  });

  // Furnished vs Unfurnished for rentals only
  this.businessRules.addRule({
    name: 'furnished-rental-only',
    fields: ['furnished', 'listingType'],
    condition: (form) => 
      form.furnished && form.listingType !== 'rent',
    message: 'Furnished/Unfurnished only applies to rentals',
    resolution: 'prevent'
  });
}

Prerequisites: - Volume 3, Pattern 9: Contextual Constraints (what's valid depends on context)

Synergies: - Volume 3, Pattern 11: Cascading Updates (selecting one updates others) - Volume 3, Pattern 13: Conditional Requirements (exclusive choices affect requirements) - Volume 3, Pattern 14: Cross-Field Validation (validate exclusive relationships)

Conflicts: - Extreme flexibility needs (when everything is an exception) - Complex domain rules (too many exclusivities)

Alternatives: - Wizard workflows (one choice per step) - Separate forms (different forms for exclusive scenarios) - Post-validation (catch conflicts at submit)

Known Uses

Payment Forms: Credit card OR PayPal OR bank transfer (radio buttons)

Shipping: Use billing address OR enter different address (toggle/radio)

Travel Booking: One-way OR round-trip OR multi-city (segmented control)

HR Forms: Full-time OR part-time OR contractor (radio buttons)

Insurance: Primary coverage OR secondary coverage (exclusive fields)

Product Configuration: Color choices, size selections (radio/dropdown)

Survey Tools (SurveyMonkey, Google Forms): Single choice vs multiple choice questions


Further Reading

Academic Foundations

  • Choice Architecture: Thaler, R.H., & Sunstein, C.R. (2008). Nudge: Improving Decisions About Health, Wealth, and Happiness. Yale University Press. ISBN: 978-0300122237
  • Constraint Logic: Apt, K.R. (2003). Principles of Constraint Programming. Cambridge University Press. ISBN: 978-0521825832
  • Radio Button Design: Nielsen, J. (2004). "Checkboxes vs. Radio Buttons." NN/g. https://www.nngroup.com/articles/checkboxes-vs-radio-buttons/

Practical Implementation

Standards & Accessibility

  • Pattern 11: Validation Rules - Enforce exclusivity
  • Pattern 14: Contextual Constraints - Exclusivity as constraint
  • Pattern 18: Conditional Requirements - Exclusive paths require different fields
  • Pattern 19: Cross-Field Validation - Validate exclusive selections
  • Volume 2, Pattern 6: Intelligent Error Recovery - Enforcing exclusive rules

Tools & Libraries

UI/UX Resources