t {
eventId: string;
userId: string;
timestamp: number;
sourceProduct: string;
targetProduct?: string;
action: 'view' | 'upgrade' | 'downgrade' | 'churn' | 'feature_use';
pricingTier: string;
migrationIntent: 'none' | 'explicit' | 'implicit';
sessionId: string;
metadata: Record<string, unknown>;
}
This schema enables downstream services to distinguish between organic adoption and substitution behavior. The `migrationIntent` field is populated by client-side heuristics (e.g., repeated visits to pricing pages, feature comparison clicks, or explicit tier switching).
### Step 2: Real-Time Attribution Pipeline
Events flow through a streaming architecture (Kafka, Redpanda, or Pulsar) where a stateful processor builds user journey graphs. The processor maintains a sliding window of cross-product interactions and calculates substitution probability.
```typescript
class AttributionEngine {
private windowMs: number = 7 * 24 * 60 * 60 * 1000; // 7 days
private substitutionThreshold: number = 0.65;
async evaluateCannibalization(event: PortfolioEvent): Promise<AttributionResult> {
const journey = await this.getUserJourney(event.userId, this.windowMs);
const substitutionScore = this.calculateSubstitutionScore(journey, event);
return {
userId: event.userId,
sourceProduct: event.sourceProduct,
targetProduct: event.targetProduct || event.sourceProduct,
substitutionProbability: substitutionScore,
shouldThrottle: substitutionScore > this.substitutionThreshold,
recommendedAction: substitutionScore > 0.85 ? 'rollback' : 'monitor'
};
}
private calculateSubstitutionScore(journey: PortfolioEvent[], newEvent: PortfolioEvent): number {
const productInteractions = journey.filter(e => e.sourceProduct !== newEvent.sourceProduct);
const pricingShifts = productInteractions.filter(e => e.action === 'upgrade' || e.action === 'downgrade').length;
const featureOverlap = this.detectFeatureOverlap(journey, newEvent);
// Weighted heuristic: pricing shifts + feature overlap + velocity
return Math.min(1, (pricingShifts * 0.4) + (featureOverlap * 0.35) + (journey.length * 0.01));
}
}
The engine uses a weighted heuristic rather than pure ML for deterministic guardrails. Production systems require explainable thresholds for rollback decisions. ML models can be layered later for trend forecasting, but real-time mitigation must rely on rule-based scoring with configurable weights.
Step 3: Automated Mitigation Middleware
The attribution result feeds into a deployment guardrail that intercepts feature flag evaluations and traffic routing decisions.
interface GuardrailConfig {
maxSubstitutionRate: number;
throttleThreshold: number;
rollbackThreshold: number;
allowedMigrationPaths: string[][];
}
class CannibalizationGuardrail {
constructor(private config: GuardrailConfig, private attribution: AttributionEngine) {}
async shouldRelease(feature: string, userId: string): Promise<ReleaseDecision> {
const event = await this.buildContextualEvent(feature, userId);
const attribution = await this.attribution.evaluateCannibalization(event);
if (attribution.substitutionProbability > this.config.rollbackThreshold) {
return { release: false, reason: 'high_cannibalization_risk', action: 'rollback' };
}
if (attribution.substitutionProbability > this.config.throttleThreshold) {
return { release: false, reason: 'moderate_cannibalization_risk', action: 'throttle' };
}
return { release: true, reason: 'within_tolerance', action: 'proceed' };
}
}
This middleware integrates with existing feature flag providers (LaunchDarkly, Unleash, or internal systems) by wrapping flag evaluation calls. It enforces portfolio-level constraints without modifying product code.
Architecture Decisions & Rationale
- Event-Driven Decoupling: Product teams publish events to a shared topic. The attribution engine consumes independently. This prevents coupling and allows schema evolution without deployment coordination.
- Stateful Stream Processing: User journey graphs require state. Using RocksDB-backed processors (Kafka Streams, Flink, or Redpanda Connect) ensures low-latency windowed aggregation without external database roundtrips.
- Deterministic Guardrails Over Pure ML: Real-time mitigation requires explainability. Heuristic scoring with configurable thresholds allows finance and engineering to align on acceptable leakage. ML is reserved for offline trend analysis and threshold calibration.
- Graph-Based Attribution: Substitution is rarely linear. Users interact with multiple products before migrating. Graph traversal (BFS/DFS over 7-day windows) captures indirect migration paths that funnel-based analytics miss.
Pitfall Guide
1. Single-Product Attribution Windows
Treating attribution windows as product-bound ignores cross-product decision cycles. Users often evaluate tiers across multiple dashboards before switching. Fix: implement portfolio-wide sliding windows with cross-entity joins.
2. Hardcoded Thresholds Without Dynamic Baselines
Static substitution probabilities fail during seasonal traffic shifts or promotional campaigns. Fix: calibrate thresholds using rolling 30-day baselines and anomaly detection. Adjust guardrail sensitivity during marketing pushes.
3. Ignoring Cohort-Based Migration Patterns
Aggregated metrics mask cohort-specific behavior. Enterprise users may migrate differently than SMB. Fix: segment attribution by plan type, geography, and acquisition channel. Apply cohort-specific guardrails.
4. Missing Rollback Integration
Detecting cannibalization without automated mitigation creates operational drag. Fix: bind guardrail decisions to CI/CD pipelines and feature flag providers. Implement kill-switches that revert traffic routing within minutes.
5. Over-Reliance on Revenue Without Engagement Signals
Revenue leakage is a lagging indicator. Users may switch products while maintaining high engagement, masking substitution until churn occurs. Fix: track feature overlap, session duration, and cross-product navigation velocity alongside ARPU.
6. Failing to Account for Indirect Cannibalization
A new feature may reduce churn for Product A while simultaneously diverting upgrades from Product B. Net revenue appears stable, but portfolio elasticity degrades. Fix: model substitution as a zero-sum matrix. Track both direct migration and retention displacement.
7. Siloed Telemetry Contracts
Product teams define event schemas independently, causing schema drift and missing cross-product fields. Fix: enforce a centralized telemetry contract with versioned schemas, automated validation, and CI checks that reject non-compliant events.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Early-stage product launch | Throttle-only guardrails | Prevents accidental migration while collecting baseline data | Low infrastructure, moderate engineering time |
| Mature portfolio with overlapping features | Predictive substitution scoring + automated rollback | Reduces revenue leakage from known feature parity | High initial setup, lowers long-term churn costs |
| Promotional campaign window | Dynamic threshold calibration + cohort segmentation | Prevents guardrail false positives during traffic spikes | Moderate compute overhead, preserves campaign ROI |
| Multi-tier pricing migration | Graph-based attribution + explicit migration paths | Tracks indirect substitution across pricing ladders | High data pipeline cost, prevents ARPU decay |
| Legacy product sunset | Forced migration routing + retirement guardrails | Eliminates cannibalization by design during phase-out | Low ongoing cost, requires frontend coordination |
Configuration Template
guardrails:
attribution:
window_ms: 604800000
scoring:
pricing_shift_weight: 0.4
feature_overlap_weight: 0.35
velocity_weight: 0.01
max_score: 1.0
thresholds:
monitor: 0.45
throttle: 0.65
rollback: 0.85
mitigation:
allowed_paths:
- ["starter", "pro"]
- ["pro", "enterprise"]
disallowed_paths:
- ["enterprise", "starter"]
- ["pro", "starter"]
calibration:
baseline_period_days: 30
anomaly_sensitivity: 2.5
campaign_mode: false
Quick Start Guide
- Install the portfolio telemetry SDK in your frontend and backend services. Configure it to emit
PortfolioEvent objects with sourceProduct, pricingTier, and migrationIntent fields.
- Deploy the attribution engine container to your streaming platform. Point it to the shared events topic and set the sliding window to 7 days.
- Add the guardrail middleware to your feature flag evaluation layer. Bind
shouldRelease() calls to your existing flag provider's SDK.
- Load the configuration template into your environment variables. Adjust thresholds based on your product overlap matrix and run a shadow-mode validation for 48 hours before enabling enforcement.