Pattern 7: Calculated Dependencies
Part II: Interaction Patterns - Intelligence Patterns
Opening Scenario: The Invoice That Didn't Add Up
Rebecca ran a small graphic design studio. She'd recently hired her first employee, Marcus, to help with client invoicing. Marcus was great at design but new to billing.
One Monday morning, a client called, confused.
"Rebecca, your invoice shows 10 hours at $150/hour, but the total is $1,200. Shouldn't that be $1,500?"
Rebecca looked at the invoice Marcus had prepared. He'd typed:
INVOICE #2024-0847
Quantity: 10
Rate: $150/hour
Subtotal: $1,200 ← Wrong
Discount (10%): $120
Tax (6%): $72
Total: $1,392
Marcus had made a mental math error: 10 × $150 = $1,500, not $1,200. Then he'd correctly calculated the 10% discount and 6% tax based on the wrong subtotal. Every downstream calculation was perfect, but they were all based on the initial error.
"Marcus, the subtotal is wrong," Rebecca said gently.
"Oh no!" Marcus pulled up the invoice template - a Word document with blank fields. He had to manually calculate every number. "I'll fix it."
He corrected the invoice:
Quantity: 10
Rate: $150/hour
Subtotal: $1,500 ← Fixed
Discount (10%): $120 ← Still wrong!
Tax (6%): $72 ← Still wrong!
Total: $1,392 ← Still wrong!
Marcus had fixed the subtotal but forgotten to recalculate the discount and tax. He'd only updated one field in the cascade.
Rebecca realized the problem wasn't Marcus - it was the process. Manual calculations meant: - Easy to make arithmetic errors - Easy to update one field but forget dependent fields - No way to verify calculations were correct - Every invoice was an opportunity for mistakes
She decided to build a proper invoice system.
Three months later, Rebecca showed me her new form:
[Invoice Form]
Line Items:
┌─────────────────────────────────────────────────────┐
│ Description: Logo design │
│ Quantity: [10] hours │
│ Rate: [$150.00] /hour │
│ Amount: $1,500.00 ← Automatically calculated │
└─────────────────────────────────────────────────────┘
[+ Add Line Item]
Subtotal: $1,500.00 ← Sum of all line items
Discount: [10]% → $150.00 ← Percentage of subtotal
Tax: 6% → $90.00 ← Percentage of taxable amount
Total: $1,440.00 ← Final computed total
[All calculations update in real-time as you type]
The form had intelligent dependencies: - Line item amount = Quantity × Rate (calculated automatically) - Subtotal = Sum of all line item amounts - Discount amount = Subtotal × Discount percentage - Taxable amount = Subtotal - Discount - Tax amount = Taxable amount × Tax rate - Total = Taxable amount + Tax amount
When Marcus typed the quantity or rate, the entire cascade recalculated instantly. No mental math. No forgotten updates. No errors.
But Rebecca went further. She encoded domain expertise into the calculations:
// Calculate sales tax based on client location
function calculateTax(amount, clientState) {
const taxRates = {
'PA': 0.06, // Pennsylvania
'NY': 0.08875, // New York
'CA': 0.0725, // California
'OR': 0, // Oregon (no sales tax)
'DE': 0, // Delaware (no sales tax)
};
const rate = taxRates[clientState] || 0;
// Services may be exempt in some states
if (isServiceOnly(lineItems) && ['PA', 'NY'].includes(clientState)) {
return {
amount: 0,
note: `Services are not subject to sales tax in ${clientState}`
};
}
return {
amount: amount * rate,
rate: rate,
note: null
};
}
// Calculate payment terms based on client history
function calculatePaymentTerms(client, invoiceAmount) {
if (client.creditRating === 'excellent' && client.invoicesPaid >= 10) {
return {
terms: 'Net 30',
dueDate: addDays(today, 30),
note: 'Thank you for being a valued client!'
};
} else if (client.pastDue > 0) {
return {
terms: 'Payment on receipt',
dueDate: today,
note: 'Previous balance must be paid before new work begins'
};
} else {
return {
terms: 'Net 15',
dueDate: addDays(today, 15),
note: null
};
}
}
// Suggest volume discount based on total hours
function suggestDiscount(totalHours, clientType) {
if (totalHours >= 40 && clientType === 'nonprofit') {
return {
suggested: 0.15, // 15% for nonprofits
reason: 'Nonprofit client + 40+ hours qualifies for 15% discount'
};
} else if (totalHours >= 100) {
return {
suggested: 0.10, // 10% for volume
reason: '100+ hours qualifies for volume discount'
};
} else if (totalHours >= 40) {
return {
suggested: 0.05, // 5% for substantial projects
reason: '40+ hours qualifies for project discount'
};
}
return { suggested: 0, reason: null };
}
Now the form didn't just calculate—it understood: - Tax rules vary by state and service type - Payment terms depend on client relationship - Discounts should be suggested based on volume
Marcus's invoices became faster, more accurate, and more professional. Client complaints dropped to zero. And Rebecca's expertise was now encoded in the system, available even when she wasn't there.
Context
Calculated Dependencies applies when:
Fields mathematically relate: Values derive from other fields (quantity × price = total)
Cascading updates needed: Changing one field should update all dependent fields
Consistency matters: Manual calculation leads to errors and inconsistencies
Domain formulas exist: Business rules define how values relate (tax rates, discounts, fees)
Real-time feedback helps: Users benefit from seeing calculations instantly
Expertise is embedded: Calculations encode expert knowledge (accountant's formulas, engineer's equations)
Complexity is manageable: Dependency graph doesn't create circular references
Problem Statement
Forms requiring calculations typically fail in several ways:
Manual calculation burden:
Quantity: [10]
Rate: [$150]
Total: [ ] ← User must calculate 10 × $150 = $1,500
Users make arithmetic errors, especially with: - Multi-step calculations - Percentages - Complex formulas - Large numbers
Forgotten cascade updates:
// User updates quantity from 10 to 15
Quantity: [15]
Rate: [$150]
Total: [$1,500] ← User forgot to recalculate!
Changing an upstream field leaves downstream fields stale.
No domain intelligence:
// Tax calculation doesn't consider location or exemptions
Tax: 6% of subtotal
// Ignores:
// - Different tax rates by state
// - Service exemptions
// - Nexus rules
// - Multiple tax jurisdictions
Generic formulas miss domain-specific rules.
Hidden dependencies:
// User doesn't know changing discount affects tax
Discount: [10%]
Tax: [Calculated on pre-discount or post-discount amount?]
Calculation order and dependencies aren't obvious.
Validation without context:
Discount: [150%] ← Accepted!
// Creates negative total, but no warning
Calculations produce nonsensical results with no guardrails.
We need forms where fields intelligently watch each other, calculate derived values automatically, encode domain expertise, and validate results in context.
Forces
Automatic vs Editable
- Most calculations should be automatic
- But sometimes users need to override
- Balance automation with flexibility
- Make overrides explicit and trackable
Real-time vs On-demand
- Real-time calculation provides instant feedback
- But can be distracting while user is typing
- Can cause performance issues with complex graphs
- Balance responsiveness with stability
Simple Formulas vs Domain Intelligence
- Simple formulas are easy to maintain
- But miss important business rules
- Domain intelligence is valuable but complex
- Need layered approach: basic math + domain rules
Client-side vs Server-side
- Client-side calculation is instant
- But can't access all data or enforce security
- Server-side is authoritative but slower
- Hybrid approach often needed
Explicit vs Implicit Dependencies
- Clear dependencies help users understand
- But make UI more complex
- Implicit calculation is cleaner
- But can feel like "magic" or be confusing
Solution
Create a dependency graph where fields declare their relationships, calculations execute automatically when dependencies change, and domain intelligence enriches basic formulas with business rules.
The pattern has five key elements:
1. Declare Field Dependencies
Make relationships explicit:
const fieldDefinitions = {
quantity: {
type: 'number',
min: 0,
default: 1
},
rate: {
type: 'currency',
min: 0,
default: 0
},
lineTotal: {
type: 'currency',
calculated: true,
dependsOn: ['quantity', 'rate'],
formula: (values) => values.quantity * values.rate,
readOnly: true
},
subtotal: {
type: 'currency',
calculated: true,
dependsOn: ['lineItems'], // Array of line items
formula: (values) => {
return values.lineItems.reduce((sum, item) =>
sum + item.lineTotal, 0
);
},
readOnly: true
},
discountPercent: {
type: 'percentage',
min: 0,
max: 100,
default: 0
},
discountAmount: {
type: 'currency',
calculated: true,
dependsOn: ['subtotal', 'discountPercent'],
formula: (values) => values.subtotal * (values.discountPercent / 100),
readOnly: true
},
taxableAmount: {
type: 'currency',
calculated: true,
dependsOn: ['subtotal', 'discountAmount'],
formula: (values) => values.subtotal - values.discountAmount,
readOnly: true
},
taxRate: {
type: 'percentage',
calculated: true,
dependsOn: ['clientState', 'lineItems'],
formula: (values, context) => {
return getTaxRate(values.clientState, values.lineItems);
},
domain: 'tax_rules'
},
taxAmount: {
type: 'currency',
calculated: true,
dependsOn: ['taxableAmount', 'taxRate'],
formula: (values) => values.taxableAmount * (values.taxRate / 100),
readOnly: true
},
total: {
type: 'currency',
calculated: true,
dependsOn: ['taxableAmount', 'taxAmount'],
formula: (values) => values.taxableAmount + values.taxAmount,
readOnly: true
}
};
2. Build Dependency Graph
Automatically construct execution order:
class DependencyGraph {
constructor(fieldDefinitions) {
this.fields = fieldDefinitions;
this.graph = this.buildGraph();
this.executionOrder = this.topologicalSort();
}
buildGraph() {
const graph = new Map();
for (const [fieldName, field] of Object.entries(this.fields)) {
if (field.calculated && field.dependsOn) {
graph.set(fieldName, field.dependsOn);
}
}
return graph;
}
topologicalSort() {
const visited = new Set();
const order = [];
const visit = (fieldName) => {
if (visited.has(fieldName)) return;
visited.add(fieldName);
const dependencies = this.graph.get(fieldName) || [];
dependencies.forEach(dep => visit(dep));
order.push(fieldName);
};
// Visit all calculated fields
for (const fieldName of this.graph.keys()) {
visit(fieldName);
}
return order;
}
getAffectedFields(changedField) {
// Find all fields that depend on changedField
const affected = [];
for (const [fieldName, dependencies] of this.graph.entries()) {
if (dependencies.includes(changedField)) {
affected.push(fieldName);
// Recursively get fields that depend on this field
affected.push(...this.getAffectedFields(fieldName));
}
}
return [...new Set(affected)]; // Remove duplicates
}
detectCircular() {
const visiting = new Set();
const visited = new Set();
const hasCycle = (fieldName) => {
if (visited.has(fieldName)) return false;
if (visiting.has(fieldName)) return true; // Cycle detected!
visiting.add(fieldName);
const dependencies = this.graph.get(fieldName) || [];
for (const dep of dependencies) {
if (hasCycle(dep)) return true;
}
visiting.delete(fieldName);
visited.add(fieldName);
return false;
};
for (const fieldName of this.graph.keys()) {
if (hasCycle(fieldName)) {
throw new Error(`Circular dependency detected involving ${fieldName}`);
}
}
}
}
3. Automatic Recalculation Engine
Execute formulas when dependencies change:
class CalculationEngine {
constructor(fieldDefinitions, context = {}) {
this.fields = fieldDefinitions;
this.graph = new DependencyGraph(fieldDefinitions);
this.values = {};
this.context = context;
this.calculationCount = 0;
// Initialize with default values
this.initialize();
// Check for circular dependencies
this.graph.detectCircular();
}
initialize() {
for (const [fieldName, field] of Object.entries(this.fields)) {
if (!field.calculated && field.default !== undefined) {
this.values[fieldName] = field.default;
}
}
// Calculate all derived fields
this.recalculateAll();
}
setValue(fieldName, value) {
const field = this.fields[fieldName];
if (field.calculated && field.readOnly) {
console.warn(`Cannot set calculated read-only field: ${fieldName}`);
return;
}
// Validate
if (!this.validateValue(fieldName, value)) {
throw new Error(`Invalid value for ${fieldName}: ${value}`);
}
// Set value
this.values[fieldName] = value;
// Recalculate affected fields
this.recalculate(fieldName);
return this.values;
}
recalculate(changedField) {
const affected = this.graph.getAffectedFields(changedField);
// Sort affected fields by execution order
const orderedFields = this.graph.executionOrder.filter(f =>
affected.includes(f)
);
// Execute calculations
for (const fieldName of orderedFields) {
this.calculateField(fieldName);
}
}
recalculateAll() {
for (const fieldName of this.graph.executionOrder) {
this.calculateField(fieldName);
}
}
calculateField(fieldName) {
const field = this.fields[fieldName];
if (!field.calculated) return;
this.calculationCount++;
try {
const result = field.formula(this.values, this.context);
this.values[fieldName] = result;
// Emit change event
this.emit('calculated', { field: fieldName, value: result });
} catch (error) {
console.error(`Error calculating ${fieldName}:`, error);
this.values[fieldName] = null;
}
}
validateValue(fieldName, value) {
const field = this.fields[fieldName];
if (field.type === 'number' || field.type === 'currency') {
if (field.min !== undefined && value < field.min) return false;
if (field.max !== undefined && value > field.max) return false;
}
if (field.type === 'percentage') {
if (value < 0 || value > 100) return false;
}
return true;
}
getValue(fieldName) {
return this.values[fieldName];
}
getAllValues() {
return { ...this.values };
}
emit(event, data) {
// Event emitter for UI updates
if (this.listeners && this.listeners[event]) {
this.listeners[event].forEach(callback => callback(data));
}
}
on(event, callback) {
if (!this.listeners) this.listeners = {};
if (!this.listeners[event]) this.listeners[event] = [];
this.listeners[event].push(callback);
}
}
4. Domain-Intelligent Formulas
Encode expert knowledge in calculations:
// Tax calculation with domain intelligence
function getTaxRate(clientState, lineItems) {
const baseTaxRates = {
'PA': 6.0,
'NY': 8.875,
'CA': 7.25,
'TX': 6.25,
'OR': 0, // No sales tax
'DE': 0 // No sales tax
};
const baseRate = baseTaxRates[clientState] || 0;
// Pennsylvania: Services are not taxable
if (clientState === 'PA' && allItemsAreServices(lineItems)) {
return 0;
}
// New York: Services taxable only in specific contexts
if (clientState === 'NY') {
const servicesRate = getNewYorkServicesRate(lineItems);
if (servicesRate !== null) return servicesRate;
}
// California: Add local tax
if (clientState === 'CA') {
const localRate = getCaliforniaLocalRate(context.clientZipCode);
return baseRate + localRate;
}
return baseRate;
}
// Discount suggestion with business rules
function suggestDiscount(values, context) {
const { subtotal, clientType, projectHours, clientHistory } = values;
let suggested = 0;
let reasons = [];
// Volume discount
if (projectHours >= 100) {
suggested = Math.max(suggested, 10);
reasons.push('100+ hours: 10% volume discount');
} else if (projectHours >= 40) {
suggested = Math.max(suggested, 5);
reasons.push('40+ hours: 5% project discount');
}
// Client type discount
if (clientType === 'nonprofit') {
suggested = Math.max(suggested, 15);
reasons.push('Nonprofit: 15% discount');
}
// Loyalty discount
if (clientHistory.invoicesPaid >= 50) {
suggested = Math.max(suggested, 10);
reasons.push('50+ invoices paid: 10% loyalty discount');
}
// Early payment incentive
if (values.paymentTerms === 'immediate') {
suggested += 2;
reasons.push('Immediate payment: +2% discount');
}
return {
percentage: Math.min(suggested, 25), // Cap at 25%
amount: subtotal * (Math.min(suggested, 25) / 100),
reasons: reasons,
requiresApproval: suggested > 15
};
}
// Payment terms with relationship intelligence
function calculatePaymentTerms(clientHistory, invoiceAmount) {
// New client
if (!clientHistory || clientHistory.invoicesPaid === 0) {
return {
terms: 'Net 15',
dueDate: addDays(new Date(), 15),
deposit: invoiceAmount >= 5000 ? invoiceAmount * 0.5 : 0,
note: 'New client: 50% deposit required for projects over $5,000'
};
}
// Client with past due balance
if (clientHistory.pastDue > 0) {
return {
terms: 'Payment on receipt',
dueDate: new Date(),
deposit: invoiceAmount + clientHistory.pastDue,
note: `Past due balance of $${clientHistory.pastDue} must be paid with current invoice`
};
}
// Excellent client
if (clientHistory.invoicesPaid >= 10 &&
clientHistory.avgPaymentDays <= 15 &&
clientHistory.creditRating === 'excellent') {
return {
terms: 'Net 30',
dueDate: addDays(new Date(), 30),
deposit: 0,
note: 'Thank you for being a valued client!'
};
}
// Standard client
return {
terms: 'Net 15',
dueDate: addDays(new Date(), 15),
deposit: 0,
note: null
};
}
// Budget allocation with constraint checking
function validateBudgetAllocation(categories, totalBudget) {
const allocated = categories.reduce((sum, cat) => sum + cat.amount, 0);
const remaining = totalBudget - allocated;
const results = {
valid: true,
messages: [],
suggestions: []
};
// Over budget
if (remaining < 0) {
results.valid = false;
results.messages.push({
severity: 'error',
text: `Budget exceeded by $${Math.abs(remaining).toFixed(2)}`,
action: 'Reduce allocations to stay within budget'
});
}
// Significant amount unallocated
if (remaining > totalBudget * 0.1) {
results.messages.push({
severity: 'warning',
text: `$${remaining.toFixed(2)} (${(remaining/totalBudget*100).toFixed(1)}%) unallocated`,
action: 'Consider allocating remaining budget'
});
}
// Check individual category constraints
categories.forEach(cat => {
// Percentage too high
if (cat.amount > totalBudget * 0.5) {
results.messages.push({
severity: 'warning',
text: `${cat.name}: ${(cat.amount/totalBudget*100).toFixed(1)}% of total budget`,
action: 'Consider diversifying budget allocation'
});
}
// Below minimum for critical category
if (cat.critical && cat.amount < cat.minimumRequired) {
results.valid = false;
results.messages.push({
severity: 'error',
text: `${cat.name} requires minimum ${cat.minimumRequired}`,
action: `Increase allocation by $${(cat.minimumRequired - cat.amount).toFixed(2)}`
});
}
});
return results;
}
5. Smart Suggestions and Warnings
Provide contextual guidance:
class SmartCalculator extends CalculationEngine {
calculateField(fieldName) {
super.calculateField(fieldName);
// After calculation, check for suggestions
const suggestions = this.getSuggestions(fieldName);
if (suggestions.length > 0) {
this.emit('suggestions', { field: fieldName, suggestions });
}
// Check for warnings
const warnings = this.getWarnings(fieldName);
if (warnings.length > 0) {
this.emit('warnings', { field: fieldName, warnings });
}
}
getSuggestions(fieldName) {
const suggestions = [];
const value = this.values[fieldName];
// Discount suggestions
if (fieldName === 'discountPercent') {
const suggested = suggestDiscount(this.values, this.context);
if (suggested.percentage > value) {
suggestions.push({
type: 'discount',
message: `Consider ${suggested.percentage}% discount`,
reasons: suggested.reasons,
action: () => this.setValue('discountPercent', suggested.percentage)
});
}
}
// Rounding suggestions
if (fieldName === 'total') {
const rounded = Math.round(value / 10) * 10;
if (Math.abs(rounded - value) < value * 0.02) { // Within 2%
suggestions.push({
type: 'rounding',
message: `Round to $${rounded}?`,
reason: 'Nice round number, easier for payment',
action: () => this.adjustToTotal(rounded)
});
}
}
return suggestions;
}
getWarnings(fieldName) {
const warnings = [];
const value = this.values[fieldName];
// Unusually high discount
if (fieldName === 'discountPercent' && value > 25) {
warnings.push({
severity: 'warning',
message: 'Discount exceeds 25%',
detail: 'Large discounts may require approval',
requiresApproval: true
});
}
// Negative total (should never happen)
if (fieldName === 'total' && value < 0) {
warnings.push({
severity: 'error',
message: 'Invoice total cannot be negative',
detail: 'Check discount and line item amounts'
});
}
// Very large invoice
if (fieldName === 'total' && value > 50000) {
warnings.push({
severity: 'info',
message: 'Large invoice amount',
detail: 'Consider splitting into multiple invoices or requiring deposit'
});
}
return warnings;
}
}
Implementation Details
Complete Invoice Calculator
// Define invoice fields with dependencies
const invoiceFields = {
// Line item fields (repeated for each line)
lineItems: {
type: 'array',
items: {
description: { type: 'text' },
quantity: { type: 'number', min: 0 },
rate: { type: 'currency', min: 0 },
amount: {
type: 'currency',
calculated: true,
dependsOn: ['quantity', 'rate'],
formula: (item) => item.quantity * item.rate
}
}
},
// Subtotal (sum of line items)
subtotal: {
type: 'currency',
calculated: true,
dependsOn: ['lineItems'],
formula: (values) => {
return values.lineItems.reduce((sum, item) =>
sum + (item.quantity * item.rate), 0
);
}
},
// Discount
discountPercent: {
type: 'percentage',
min: 0,
max: 100,
default: 0
},
discountAmount: {
type: 'currency',
calculated: true,
dependsOn: ['subtotal', 'discountPercent'],
formula: (values) => values.subtotal * (values.discountPercent / 100)
},
// Taxable amount
taxableAmount: {
type: 'currency',
calculated: true,
dependsOn: ['subtotal', 'discountAmount'],
formula: (values) => values.subtotal - values.discountAmount
},
// Tax rate (domain-intelligent)
taxRate: {
type: 'percentage',
calculated: true,
dependsOn: ['clientState', 'lineItems'],
formula: (values, context) => {
return getTaxRate(values.clientState, values.lineItems, context);
}
},
// Tax amount
taxAmount: {
type: 'currency',
calculated: true,
dependsOn: ['taxableAmount', 'taxRate'],
formula: (values) => values.taxableAmount * (values.taxRate / 100)
},
// Total
total: {
type: 'currency',
calculated: true,
dependsOn: ['taxableAmount', 'taxAmount'],
formula: (values) => values.taxableAmount + values.taxAmount
},
// Payment terms (domain-intelligent)
paymentTerms: {
type: 'text',
calculated: true,
dependsOn: ['clientHistory', 'total'],
formula: (values, context) => {
const terms = calculatePaymentTerms(values.clientHistory, values.total);
return terms.terms;
}
},
dueDate: {
type: 'date',
calculated: true,
dependsOn: ['paymentTerms'],
formula: (values, context) => {
const terms = calculatePaymentTerms(values.clientHistory, values.total);
return terms.dueDate;
}
}
};
// Create calculator
const invoice = new SmartCalculator(invoiceFields, {
clientState: 'PA',
clientHistory: {
invoicesPaid: 15,
avgPaymentDays: 12,
creditRating: 'excellent',
pastDue: 0
}
});
// Listen for calculation events
invoice.on('calculated', ({ field, value }) => {
console.log(`${field} calculated: ${value}`);
updateUI(field, value);
});
invoice.on('suggestions', ({ field, suggestions }) => {
displaySuggestions(suggestions);
});
invoice.on('warnings', ({ field, warnings }) => {
displayWarnings(warnings);
});
// User interaction
invoice.setValue('lineItems', [
{ description: 'Logo design', quantity: 10, rate: 150 },
{ description: 'Website mockup', quantity: 20, rate: 125 }
]);
invoice.setValue('discountPercent', 10);
// Get final values
const finalInvoice = invoice.getAllValues();
console.log('Total:', finalInvoice.total);
console.log('Due date:', finalInvoice.dueDate);
Consequences
Benefits
Eliminates Arithmetic Errors: - No manual calculation mistakes - Cascade updates happen automatically - Consistency guaranteed across dependent fields
Encodes Expert Knowledge: - Tax rules, discount policies, payment terms - Business logic preserved in formulas - Expertise available even when expert isn't
Instant Feedback: - Users see results immediately - Can experiment with values - Understand impact of changes in real-time
Reduced Cognitive Load: - System handles complexity - Users focus on decisions, not math - Mental energy saved for judgment calls
Improved Data Quality: - Calculations always correct - No inconsistent states - Validation catches impossible results
Better User Experience: - Form feels intelligent - Suggestions guide good decisions - Professional, polished output
Liabilities
Implementation Complexity: - Dependency graph requires careful design - Circular dependencies must be avoided - Testing all calculation paths is challenging
Performance Concerns: - Complex graphs can be slow to recalculate - May need optimization for large forms - Mobile devices may struggle with heavy calculations
Maintenance Burden: - Business rules change (tax rates, policies) - Formulas must be updated - Testing after changes is critical
Over-automation Risk: - Users may not understand calculations - Hard to troubleshoot when wrong - May need "show work" feature
Hidden Logic: - Calculated fields can feel like "magic" - Users may not trust automated values - Transparency needed for complex formulas
Brittleness: - Changes to field structure break dependencies - Adding fields may require formula updates - Needs versioning strategy
Domain Examples
Accounting: Budget Allocation
Budget planning with constraints:
const budgetFields = {
totalBudget: {
type: 'currency',
default: 100000
},
categories: {
type: 'array',
items: {
name: { type: 'text' },
allocated: { type: 'currency', min: 0 },
percentage: {
type: 'percentage',
calculated: true,
dependsOn: ['allocated', 'totalBudget'],
formula: (item, values) =>
(item.allocated / values.totalBudget) * 100
}
}
},
totalAllocated: {
type: 'currency',
calculated: true,
dependsOn: ['categories'],
formula: (values) =>
values.categories.reduce((sum, cat) => sum + cat.allocated, 0)
},
remaining: {
type: 'currency',
calculated: true,
dependsOn: ['totalBudget', 'totalAllocated'],
formula: (values) => values.totalBudget - values.totalAllocated,
validation: (value) => ({
valid: value >= 0,
message: value < 0 ?
`Over budget by $${Math.abs(value).toFixed(2)}` : null
})
},
percentageRemaining: {
type: 'percentage',
calculated: true,
dependsOn: ['remaining', 'totalBudget'],
formula: (values) => (values.remaining / values.totalBudget) * 100
}
};
Healthcare: Medication Dosage
Dosage calculation with safety checks:
const dosageFields = {
patientWeight: {
type: 'number',
unit: 'kg',
min: 0,
max: 300
},
medicationConcentration: {
type: 'number',
unit: 'mg/mL',
default: 10
},
dosePerKg: {
type: 'number',
unit: 'mg/kg',
default: 5
},
totalDoseMg: {
type: 'number',
calculated: true,
dependsOn: ['patientWeight', 'dosePerKg'],
formula: (values) => values.patientWeight * values.dosePerKg,
validation: (value, values) => {
const maxDose = values.maximumDailyDose || Infinity;
return {
valid: value <= maxDose,
message: value > maxDose ?
`Dose exceeds maximum daily dose of ${maxDose}mg` : null,
severity: 'error'
};
}
},
volumeToAdminister: {
type: 'number',
unit: 'mL',
calculated: true,
dependsOn: ['totalDoseMg', 'medicationConcentration'],
formula: (values) =>
values.totalDoseMg / values.medicationConcentration,
precision: 1, // Round to 1 decimal place
validation: (value) => {
if (value < 0.1) {
return {
valid: false,
message: 'Volume too small to measure accurately',
severity: 'error'
};
}
if (value > 50) {
return {
valid: true,
message: 'Large volume - consider dividing into multiple doses',
severity: 'warning'
};
}
return { valid: true };
}
}
};
Construction: Material Estimation
Construction materials with waste factors:
const materialsFields = {
roomLength: { type: 'number', unit: 'feet', min: 0 },
roomWidth: { type: 'number', unit: 'feet', min: 0 },
floorArea: {
type: 'number',
unit: 'sq ft',
calculated: true,
dependsOn: ['roomLength', 'roomWidth'],
formula: (values) => values.roomLength * values.roomWidth
},
tileSize: {
type: 'number',
unit: 'inches',
default: 12,
options: [6, 8, 12, 16, 18, 24]
},
tileCoverage: {
type: 'number',
unit: 'sq ft per tile',
calculated: true,
dependsOn: ['tileSize'],
formula: (values) => (values.tileSize * values.tileSize) / 144
},
wasteFactor: {
type: 'percentage',
calculated: true,
dependsOn: ['roomShape', 'tilePattern'],
formula: (values) => {
const baseWaste = {
'rectangular': 5,
'L-shaped': 10,
'irregular': 15
}[values.roomShape] || 10;
const patternWaste = {
'straight': 0,
'diagonal': 5,
'herringbone': 10,
'basketweave': 8
}[values.tilePattern] || 0;
return baseWaste + patternWaste;
}
},
tilesNeeded: {
type: 'number',
calculated: true,
dependsOn: ['floorArea', 'tileCoverage', 'wasteFactor'],
formula: (values) => {
const base = values.floorArea / values.tileCoverage;
const withWaste = base * (1 + values.wasteFactor / 100);
return Math.ceil(withWaste);
}
},
boxesNeeded: {
type: 'number',
calculated: true,
dependsOn: ['tilesNeeded', 'tilesPerBox'],
formula: (values) =>
Math.ceil(values.tilesNeeded / values.tilesPerBox)
},
totalCost: {
type: 'currency',
calculated: true,
dependsOn: ['boxesNeeded', 'pricePerBox'],
formula: (values) => values.boxesNeeded * values.pricePerBox
}
};
Real Estate: Mortgage Calculation
Loan payment with amortization:
const mortgageFields = {
purchasePrice: { type: 'currency', min: 0 },
downPaymentPercent: { type: 'percentage', min: 0, max: 100, default: 20 },
downPaymentAmount: {
type: 'currency',
calculated: true,
dependsOn: ['purchasePrice', 'downPaymentPercent'],
formula: (values) =>
values.purchasePrice * (values.downPaymentPercent / 100)
},
loanAmount: {
type: 'currency',
calculated: true,
dependsOn: ['purchasePrice', 'downPaymentAmount'],
formula: (values) => values.purchasePrice - values.downPaymentAmount
},
interestRate: { type: 'percentage', min: 0, max: 30, default: 6.5 },
loanTermYears: { type: 'number', default: 30, options: [15, 20, 30] },
monthlyPayment: {
type: 'currency',
calculated: true,
dependsOn: ['loanAmount', 'interestRate', 'loanTermYears'],
formula: (values) => {
const principal = values.loanAmount;
const monthlyRate = (values.interestRate / 100) / 12;
const numPayments = values.loanTermYears * 12;
if (monthlyRate === 0) return principal / numPayments;
const payment = principal *
(monthlyRate * Math.pow(1 + monthlyRate, numPayments)) /
(Math.pow(1 + monthlyRate, numPayments) - 1);
return payment;
}
},
pmiRequired: {
type: 'boolean',
calculated: true,
dependsOn: ['downPaymentPercent'],
formula: (values) => values.downPaymentPercent < 20
},
pmiAmount: {
type: 'currency',
calculated: true,
dependsOn: ['loanAmount', 'pmiRequired'],
formula: (values) => {
if (!values.pmiRequired) return 0;
return (values.loanAmount * 0.007) / 12; // 0.7% annually
}
},
propertyTaxMonthly: {
type: 'currency',
calculated: true,
dependsOn: ['purchasePrice', 'propertyTaxRate'],
formula: (values) =>
(values.purchasePrice * (values.propertyTaxRate / 100)) / 12
},
insuranceMonthly: {
type: 'currency',
calculated: true,
dependsOn: ['purchasePrice'],
formula: (values) => (values.purchasePrice * 0.005) / 12
},
totalMonthlyPayment: {
type: 'currency',
calculated: true,
dependsOn: ['monthlyPayment', 'pmiAmount', 'propertyTaxMonthly', 'insuranceMonthly'],
formula: (values) =>
values.monthlyPayment +
values.pmiAmount +
values.propertyTaxMonthly +
values.insuranceMonthly
},
totalInterestPaid: {
type: 'currency',
calculated: true,
dependsOn: ['monthlyPayment', 'loanTermYears', 'loanAmount'],
formula: (values) => {
const totalPaid = values.monthlyPayment * values.loanTermYears * 12;
return totalPaid - values.loanAmount;
}
}
};
Legal: Billable Hours & Fees
Legal time tracking with billing rules:
const billingFields = {
timeEntries: {
type: 'array',
items: {
attorney: { type: 'text' },
hours: { type: 'number', min: 0, precision: 0.1 },
rate: { type: 'currency', min: 0 },
description: { type: 'text' },
amount: {
type: 'currency',
calculated: true,
dependsOn: ['hours', 'rate'],
formula: (entry) => entry.hours * entry.rate
}
}
},
totalHours: {
type: 'number',
calculated: true,
dependsOn: ['timeEntries'],
formula: (values) =>
values.timeEntries.reduce((sum, entry) => sum + entry.hours, 0),
precision: 1
},
totalFees: {
type: 'currency',
calculated: true,
dependsOn: ['timeEntries'],
formula: (values) =>
values.timeEntries.reduce((sum, entry) =>
sum + (entry.hours * entry.rate), 0
)
},
flatFeeCap: { type: 'currency', default: null },
cappedFees: {
type: 'currency',
calculated: true,
dependsOn: ['totalFees', 'flatFeeCap'],
formula: (values) => {
if (!values.flatFeeCap) return values.totalFees;
return Math.min(values.totalFees, values.flatFeeCap);
}
},
discountApplied: {
type: 'currency',
calculated: true,
dependsOn: ['totalFees', 'cappedFees'],
formula: (values) => values.totalFees - values.cappedFees
},
clientTypeDiscount: {
type: 'percentage',
calculated: true,
dependsOn: ['clientType'],
formula: (values) => {
const discounts = {
'nonprofit': 15,
'pro_bono': 100,
'government': 10,
'corporate': 0
};
return discounts[values.clientType] || 0;
}
},
discountAmount: {
type: 'currency',
calculated: true,
dependsOn: ['cappedFees', 'clientTypeDiscount'],
formula: (values) =>
values.cappedFees * (values.clientTypeDiscount / 100)
},
finalFees: {
type: 'currency',
calculated: true,
dependsOn: ['cappedFees', 'discountAmount'],
formula: (values) => values.cappedFees - values.discountAmount
}
};
Related Patterns
Prerequisites: - Volume 3, Pattern 6: Domain-Aware Validation (calculated values must be validated)
Synergies: - Volume 3, Pattern 5: Error as Collaboration (calculation errors need good messaging) - Volume 3, Pattern 8: Intelligent Defaults (can pre-populate based on calculations) - Volume 3, Pattern 9: Contextual Constraints (calculations affect what's valid) - Volume 3, Pattern 14: Cross-Field Validation (calculations often involve multiple fields)
Conflicts: - Manual override requirements (some fields need both calculation and manual entry) - Audit trail needs (must track when calculated values change)
Alternatives: - Post-entry calculation (calculate on submit rather than real-time) - Server-side only (if client-side performance is an issue) - Spreadsheet-style (if users need formula visibility and editing)
Known Uses
TurboTax/Tax Software: Extensive calculation graphs for tax forms (deductions, credits, dependencies)
Invoice/Billing Systems (QuickBooks, FreshBooks): Line items, discounts, taxes calculate automatically
Mortgage Calculators: Purchase price → loan amount → monthly payment → total interest
Healthcare Portals: BMI calculation, medication dosage, lab result interpretation
Construction Estimators: Material quantities, waste factors, labor hours, total cost
E-commerce Carts: Item totals, shipping, taxes, discounts, final total
Time Tracking Systems (Harvest, Toggl): Hours × rate = billable amount, with overtime rules
Financial Planners: Retirement savings projections, compound interest, future value calculations
Further Reading
On Reactive Programming: - "Introduction to Reactive Programming." https://gist.github.com/staltz/868e7e9bc2a7b8c1f754 (Mental model for reactive calculations) - Vue.js Reactivity Documentation: https://vuejs.org/guide/essentials/reactivity-fundamentals.html (Computed properties pattern) - MobX Documentation: https://mobx.js.org/README.html (Observable state and automatic reactions) - RxJS Documentation: https://rxjs.dev/guide/overview (Reactive extensions for complex flows)
On Dependency Graphs: - Kahn, A. B. "Topological Sorting of Large Networks." Communications of the ACM, 1962. (Classic algorithm for dependency ordering) - "Directed Acyclic Graphs in Practice." https://en.wikipedia.org/wiki/Directed_acyclic_graph (Understanding calculation dependencies)
On Form Calculations: - MDN Web Docs. "Client-side Form Validation." https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation (Foundation for validation after calculation) - "Excel Formula Best Practices." https://support.microsoft.com/en-us/office/excel-formula-best-practices (Lessons from spreadsheet design)
Libraries & Tools: - Math.js: https://mathjs.org/ (Comprehensive math library for JavaScript) - Algebrite: http://algebrite.org/ (Symbolic algebra in JavaScript) - Decimal.js: https://github.com/MikeMcl/decimal.js/ (Precise decimal arithmetic for financial calculations) - Formula.js: https://formulajs.info/ (Excel formula compatibility for JavaScript)
Real-World Examples: - Stripe Tax Calculator: https://stripe.com/docs/tax/calculation (Complex tax calculation dependencies) - Shopify Discounts Engine: https://help.shopify.com/en/manual/discounts (Cascading discount logic)
Related Trilogy Patterns
- Pattern 6: Domain-Aware Validation - Calculated values must be validated
- Pattern 14: Cross-Field Validation - Calculations often involve multiple fields
- Pattern 5: Error as Collaboration - Calculation errors need good messaging
- Pattern 8: Intelligent Defaults - Can pre-populate based on calculations
- Pattern 9: Contextual Constraints - Calculations affect what's valid
- Volume 2, Pattern 1: Universal Event Log - Track calculation changes for pattern mining
- Volume 2, Pattern 16: Cohort Discovery & Analysis - Discover which calculations confuse users
- Volume 1, Chapter 9: User Experience Design - Understanding data dependencies