Let me share a cool moment from when I was A/B testing new features on my blog platform. I wanted /dashboard to show different versions based on user segments. New users get the beta dashboard, veterans get the classic one. But I didn't want complex if-else logic everywhere. Enter CanMatch (Angular 14+): it decides which route matches instead of blocking navigation. Genius!
The Route Selection Problem I Solved
Same URL /dashboard, different experiences:
New user → /dashboard → BetaDashboardComponent (v2)
Veteran → /dashboard → ClassicDashboardComponent (v1)
Premium → /dashboard → ProDashboardComponent
CanMatch makes multiple routes compete for the same path. Winner takes all!
My Unique CanMatch Implementation
Step 1: User Segmentation Service
// user-segment.service.ts
@Injectable({ providedIn: 'root' })
export class UserSegmentService {
getUserSegment(): 'newbie' | 'veteran' | 'premium' {
const daysSinceSignup = this.getDaysSinceSignup();
const subscription = localStorage.getItem('subscription');
if (daysSinceSignup < 7) return 'newbie';
if (subscription === 'pro-plan') return 'premium';
return 'veteran';
}
private getDaysSinceSignup(): number {
const signupDate = localStorage.getItem('signupDate');
if (!signupDate) return 1;
return Math.floor((Date.now() - parseInt(signupDate)) / (1000 * 60 * 60 * 24));
}
}
Step 2: Create Three Competing CanMatch Guards
// newbie-match.guard.ts
@Injectable({ providedIn: 'root' })
export class NewbieCanMatchGuard implements CanMatch {
constructor(private userSegment: UserSegmentService) {}
canMatch(): boolean {
return this.userSegment.getUserSegment() === 'newbie';
}
}
// veteran-match.guard.ts
@Injectable({ providedIn: 'root' })
export class VeteranCanMatchGuard implements CanMatch {
constructor(private userSegment: UserSegmentService) {}
canMatch(): boolean {
return this.userSegment.getUserSegment() === 'veteran';
}
}
// premium-match.guard.ts
@Injectable({ providedIn: 'root' })
export class PremiumCanMatchGuard implements CanMatch {
constructor(private userSegment: UserSegmentService) {}
canMatch(): boolean {
return this.userSegment.getUserSegment() === 'premium';
}
}
Step 3: Competing Routes (Magic Happens Here!)
// app-routing.module.ts
const routes: Routes = [
{ path: '', component: HomeComponent },
// SAME PATH, different guards/components!
{
path: 'dashboard',
canMatch: [NewbieCanMatchGuard],
loadComponent: () => import('./beta-dashboard.component')
},
{
path: 'dashboard',
canMatch: [VeteranCanMatchGuard],
loadComponent: () => import('./classic-dashboard.component')
},
{
path: 'dashboard',
canMatch: [PremiumCanMatchGuard],
loadComponent: () => import('./pro-dashboard.component')
},
{ path: '**', component: NotFoundComponent }
];
Testing My A/B Dashboard Setup
LocalStorage signupDate: yesterday → BetaDashboard loads
LocalStorage subscription: pro-plan → ProDashboard loads
No signupDate → ClassicDashboard (veteran) loads
Console: Only ONE route matches!
CanMatch vs Other Guards (My Findings)
CanMatch ❌ → Skips route, tries next match
CanActivate ❌ → Blocks navigation entirely
CanLoad ❌ → Prevents lazy module download
Key: CanMatch is for route selection, not blocking. Perfect for A/B tests, feature flags, and role-based UIs!
Real-World Wins From My Deploy
- Zero Code Duplication: Same /dashboard URL, different experiences
- Analytics Ready: Track which dashboard version each user sees
- Gradual Rollouts: 10% users → NewbieCanMatchGuard condition
- Performance: Lazy loads only the winning component
- Clean URLs: No query params like ?version=beta
