Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ Chain of Responsibility: Request Handling
๐ฏ Introduction
Ever been to a help desk where your request gets passed from one person to another until someone can help? Thatโs the Chain of Responsibility pattern in action! ๐ญ In this tutorial, weโll explore how to implement this powerful pattern in TypeScript to create flexible, maintainable request handling systems.
Imagine building a customer support system where different types of requests need different handlers - some can be handled by bots ๐ค, others need human agents ๐ค, and critical issues go straight to supervisors ๐จโ๐ผ. The Chain of Responsibility pattern makes this elegantly simple!
By the end of this tutorial, youโll be creating request handling chains that would make any support team jealous! Letโs dive in! ๐โโ๏ธ
๐ Understanding Chain of Responsibility
The Chain of Responsibility pattern is like a game of โhot potatoโ ๐ฅ where each player (handler) decides whether to handle the potato (request) or pass it to the next player. Each handler in the chain has two choices:
- Handle the request and stop the chain โ
- Pass it to the next handler in line โก๏ธ
Think of it as a series of filters or checkpoints. Each checkpoint examines the request and decides: โIs this for me? If yes, Iโll handle it. If not, next please!โ ๐ซ
In TypeScript, we create a chain of handler objects, each knowing about the next handler. When a request comes in, it travels through the chain until someone handles it or it reaches the end.
๐ง Basic Syntax and Usage
Letโs start with a simple example - a support ticket system! ๐ซ
// ๐ฏ Define our request type
interface SupportTicket {
id: string;
priority: 'low' | 'medium' | 'high' | 'critical';
issue: string;
customerType: 'regular' | 'premium' | 'vip';
}
// ๐๏ธ Abstract handler class
abstract class SupportHandler {
private nextHandler: SupportHandler | null = null;
// ๐ Set the next handler in chain
setNext(handler: SupportHandler): SupportHandler {
this.nextHandler = handler;
return handler; // ๐ก Return for chaining!
}
// ๐จ Handle the request
handle(ticket: SupportTicket): string {
const result = this.processTicket(ticket);
if (result) {
return result;
}
if (this.nextHandler) {
return this.nextHandler.handle(ticket);
}
return "โ ๏ธ No handler available for this ticket!";
}
// ๐ฏ Each handler implements this
protected abstract processTicket(ticket: SupportTicket): string | null;
}
// ๐ค Bot handler for simple issues
class BotHandler extends SupportHandler {
protected processTicket(ticket: SupportTicket): string | null {
if (ticket.priority === 'low') {
return `๐ค Bot handled ticket ${ticket.id}: ${ticket.issue}`;
}
return null; // ๐ Pass to next handler
}
}
// ๐ค Human agent handler
class AgentHandler extends SupportHandler {
protected processTicket(ticket: SupportTicket): string | null {
if (ticket.priority === 'medium' || ticket.priority === 'high') {
return `๐ค Agent handled ticket ${ticket.id}: ${ticket.issue}`;
}
return null;
}
}
// ๐จโ๐ผ Supervisor handler for critical issues
class SupervisorHandler extends SupportHandler {
protected processTicket(ticket: SupportTicket): string | null {
if (ticket.priority === 'critical' || ticket.customerType === 'vip') {
return `๐จโ๐ผ Supervisor handled ticket ${ticket.id}: ${ticket.issue}`;
}
return null;
}
}
๐ก Practical Examples
Example 1: Customer Support System ๐ง
Letโs build a complete support system with different handling strategies!
// ๐ฏ Enhanced ticket system with more features
interface EnhancedTicket extends SupportTicket {
category: 'billing' | 'technical' | 'general' | 'complaint';
responseTime: number; // in minutes
resolved: boolean;
}
// ๐ฐ Billing specialist handler
class BillingHandler extends SupportHandler {
protected processTicket(ticket: EnhancedTicket): string | null {
if (ticket.category === 'billing') {
console.log(`๐ฐ Processing billing issue...`);
return `๐ณ Billing team resolved: ${ticket.issue}`;
}
return null;
}
}
// ๐ง Technical support handler
class TechnicalHandler extends SupportHandler {
private readonly expertise = ['login', 'performance', 'bug', 'crash'];
protected processTicket(ticket: EnhancedTicket): string | null {
if (ticket.category === 'technical') {
const canHandle = this.expertise.some(keyword =>
ticket.issue.toLowerCase().includes(keyword)
);
if (canHandle) {
return `๐ง Tech support fixed: ${ticket.issue}`;
}
}
return null;
}
}
// ๐จ Escalation handler for complaints
class EscalationHandler extends SupportHandler {
protected processTicket(ticket: EnhancedTicket): string | null {
if (ticket.category === 'complaint' ||
ticket.responseTime > 60 ||
ticket.customerType === 'vip') {
return `๐จ Escalated to management: ${ticket.issue}`;
}
return null;
}
}
// ๐๏ธ Build the support chain
const buildSupportChain = (): SupportHandler => {
const bot = new BotHandler();
const billing = new BillingHandler();
const tech = new TechnicalHandler();
const agent = new AgentHandler();
const escalation = new EscalationHandler();
const supervisor = new SupervisorHandler();
// ๐ Chain them together!
bot.setNext(billing)
.setNext(tech)
.setNext(agent)
.setNext(escalation)
.setNext(supervisor);
return bot; // ๐ฏ Return first handler
};
// ๐ฎ Let's test it!
const supportChain = buildSupportChain();
const tickets: EnhancedTicket[] = [
{
id: "T001",
priority: 'low',
issue: "How do I reset my password?",
customerType: 'regular',
category: 'general',
responseTime: 5,
resolved: false
},
{
id: "T002",
priority: 'high',
issue: "Billing error on my account",
customerType: 'premium',
category: 'billing',
responseTime: 15,
resolved: false
},
{
id: "T003",
priority: 'critical',
issue: "System crash losing customer data!",
customerType: 'regular',
category: 'technical',
responseTime: 120,
resolved: false
}
];
tickets.forEach(ticket => {
console.log(supportChain.handle(ticket));
});
Example 2: Authentication Chain ๐
Letโs create a security chain for authentication!
// ๐ Authentication request
interface AuthRequest {
username: string;
password?: string;
token?: string;
biometric?: string;
ipAddress: string;
deviceId: string;
}
interface AuthResult {
success: boolean;
method: string;
message: string;
userId?: string;
}
// ๐๏ธ Authentication handler base
abstract class AuthHandler {
private next: AuthHandler | null = null;
setNext(handler: AuthHandler): AuthHandler {
this.next = handler;
return handler;
}
async authenticate(request: AuthRequest): Promise<AuthResult> {
const result = await this.tryAuthenticate(request);
if (result.success) {
return result;
}
if (this.next) {
return this.next.authenticate(request);
}
return {
success: false,
method: 'none',
message: 'โ All authentication methods failed'
};
}
protected abstract tryAuthenticate(request: AuthRequest): Promise<AuthResult>;
}
// ๐ Password authentication
class PasswordAuthHandler extends AuthHandler {
private users = new Map([
['alice', { password: 'secure123', id: 'U001' }],
['bob', { password: 'secret456', id: 'U002' }]
]);
protected async tryAuthenticate(request: AuthRequest): Promise<AuthResult> {
if (!request.password) {
return { success: false, method: 'password', message: 'No password provided' };
}
const user = this.users.get(request.username);
if (user && user.password === request.password) {
console.log(`๐ Password auth successful for ${request.username}`);
return {
success: true,
method: 'password',
message: 'โ
Password authentication successful',
userId: user.id
};
}
return { success: false, method: 'password', message: 'Invalid credentials' };
}
}
// ๐ซ Token authentication
class TokenAuthHandler extends AuthHandler {
private validTokens = new Map([
['TOKEN-123', { userId: 'U001', expires: Date.now() + 3600000 }],
['TOKEN-456', { userId: 'U002', expires: Date.now() + 3600000 }]
]);
protected async tryAuthenticate(request: AuthRequest): Promise<AuthResult> {
if (!request.token) {
return { success: false, method: 'token', message: 'No token provided' };
}
const tokenData = this.validTokens.get(request.token);
if (tokenData && tokenData.expires > Date.now()) {
console.log(`๐ซ Token auth successful`);
return {
success: true,
method: 'token',
message: 'โ
Token authentication successful',
userId: tokenData.userId
};
}
return { success: false, method: 'token', message: 'Invalid or expired token' };
}
}
// ๐ Biometric authentication
class BiometricAuthHandler extends AuthHandler {
private biometricData = new Map([
['FINGER-001', 'U001'],
['FACE-002', 'U002']
]);
protected async tryAuthenticate(request: AuthRequest): Promise<AuthResult> {
if (!request.biometric) {
return { success: false, method: 'biometric', message: 'No biometric data' };
}
const userId = this.biometricData.get(request.biometric);
if (userId) {
console.log(`๐ Biometric auth successful`);
return {
success: true,
method: 'biometric',
message: 'โ
Biometric authentication successful',
userId
};
}
return { success: false, method: 'biometric', message: 'Biometric not recognized' };
}
}
// ๐ IP-based authentication (for trusted networks)
class IPAuthHandler extends AuthHandler {
private trustedIPs = ['192.168.1.100', '10.0.0.50'];
protected async tryAuthenticate(request: AuthRequest): Promise<AuthResult> {
if (this.trustedIPs.includes(request.ipAddress)) {
console.log(`๐ Trusted IP authentication`);
return {
success: true,
method: 'ip',
message: 'โ
Trusted IP authentication',
userId: 'guest'
};
}
return { success: false, method: 'ip', message: 'Untrusted IP address' };
}
}
Example 3: Request Validation Pipeline ๐ฆ
Letโs build a validation chain for API requests!
// ๐ฆ API Request type
interface APIRequest {
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
path: string;
headers: Record<string, string>;
body?: unknown;
apiKey?: string;
timestamp: number;
}
interface ValidationResult {
valid: boolean;
error?: string;
processedBy: string;
}
// ๐๏ธ Validation handler base
abstract class ValidationHandler {
private next: ValidationHandler | null = null;
setNext(handler: ValidationHandler): ValidationHandler {
this.next = handler;
return handler;
}
validate(request: APIRequest): ValidationResult {
const result = this.performValidation(request);
if (!result.valid) {
return result; // ๐ Stop on first failure
}
if (this.next) {
return this.next.validate(request);
}
return { valid: true, processedBy: 'all' };
}
protected abstract performValidation(request: APIRequest): ValidationResult;
}
// ๐ API Key validator
class APIKeyValidator extends ValidationHandler {
private validKeys = new Set(['KEY-123', 'KEY-456', 'KEY-789']);
protected performValidation(request: APIRequest): ValidationResult {
if (!request.apiKey) {
return {
valid: false,
error: '๐ API key required',
processedBy: 'APIKeyValidator'
};
}
if (!this.validKeys.has(request.apiKey)) {
return {
valid: false,
error: 'โ Invalid API key',
processedBy: 'APIKeyValidator'
};
}
console.log('โ
API key validated');
return { valid: true, processedBy: 'APIKeyValidator' };
}
}
// โฐ Rate limit validator
class RateLimitValidator extends ValidationHandler {
private requests = new Map<string, number[]>();
private readonly limit = 10; // 10 requests per minute
protected performValidation(request: APIRequest): ValidationResult {
const key = request.apiKey || 'anonymous';
const now = Date.now();
const minute = 60000;
// ๐งน Clean old requests
const userRequests = this.requests.get(key) || [];
const recentRequests = userRequests.filter(time => now - time < minute);
if (recentRequests.length >= this.limit) {
return {
valid: false,
error: `โฐ Rate limit exceeded (${this.limit}/min)`,
processedBy: 'RateLimitValidator'
};
}
recentRequests.push(now);
this.requests.set(key, recentRequests);
console.log(`โ
Rate limit OK (${recentRequests.length}/${this.limit})`);
return { valid: true, processedBy: 'RateLimitValidator' };
}
}
// ๐ Content validator
class ContentValidator extends ValidationHandler {
protected performValidation(request: APIRequest): ValidationResult {
// ๐ซ Check content type for POST/PUT
if (['POST', 'PUT'].includes(request.method)) {
if (!request.headers['content-type']) {
return {
valid: false,
error: '๐ Content-Type header required',
processedBy: 'ContentValidator'
};
}
if (!request.body) {
return {
valid: false,
error: '๐ฆ Request body required',
processedBy: 'ContentValidator'
};
}
}
// ๐ Validate JSON body
if (request.headers['content-type']?.includes('application/json')) {
try {
if (typeof request.body === 'string') {
JSON.parse(request.body);
}
} catch {
return {
valid: false,
error: 'โ Invalid JSON body',
processedBy: 'ContentValidator'
};
}
}
console.log('โ
Content validated');
return { valid: true, processedBy: 'ContentValidator' };
}
}
// ๐ก๏ธ Security validator
class SecurityValidator extends ValidationHandler {
private blockedPaths = ['/admin', '/internal', '/.env'];
private sqlPatterns = /(\bselect\b|\binsert\b|\bdelete\b|\bdrop\b)/i;
protected performValidation(request: APIRequest): ValidationResult {
// ๐ซ Check blocked paths
if (this.blockedPaths.some(path => request.path.includes(path))) {
return {
valid: false,
error: '๐ก๏ธ Access denied to restricted path',
processedBy: 'SecurityValidator'
};
}
// ๐ Check for SQL injection attempts
const checkString = `${request.path} ${JSON.stringify(request.body || '')}`;
if (this.sqlPatterns.test(checkString)) {
return {
valid: false,
error: 'โ ๏ธ Potential SQL injection detected',
processedBy: 'SecurityValidator'
};
}
console.log('โ
Security check passed');
return { valid: true, processedBy: 'SecurityValidator' };
}
}
// ๐ฎ Test the validation chain!
const buildValidationChain = (): ValidationHandler => {
const apiKey = new APIKeyValidator();
const rateLimit = new RateLimitValidator();
const content = new ContentValidator();
const security = new SecurityValidator();
apiKey.setNext(rateLimit)
.setNext(security)
.setNext(content);
return apiKey;
};
const validator = buildValidationChain();
// ๐งช Test various requests
const testRequests: APIRequest[] = [
{
method: 'GET',
path: '/api/users',
headers: {},
timestamp: Date.now()
},
{
method: 'POST',
path: '/api/users',
headers: { 'content-type': 'application/json' },
body: { name: 'Alice ๐' },
apiKey: 'KEY-123',
timestamp: Date.now()
},
{
method: 'DELETE',
path: '/admin/users',
headers: {},
apiKey: 'KEY-456',
timestamp: Date.now()
}
];
testRequests.forEach((req, i) => {
console.log(`\n๐ Testing request ${i + 1}:`);
const result = validator.validate(req);
console.log(result);
});
๐ Advanced Concepts
Dynamic Chain Building ๐๏ธ
Letโs create a system that builds chains dynamically based on configuration!
// ๐ฏ Generic handler with priority
interface HandlerConfig {
name: string;
priority: number;
enabled: boolean;
conditions?: Record<string, unknown>;
}
// ๐ญ Handler factory
class HandlerFactory<T> {
private handlers = new Map<string, new() => Handler<T>>();
register(name: string, HandlerClass: new() => Handler<T>): void {
this.handlers.set(name, HandlerClass);
}
create(name: string): Handler<T> | null {
const HandlerClass = this.handlers.get(name);
return HandlerClass ? new HandlerClass() : null;
}
}
// ๐ Advanced handler with middleware support
abstract class Handler<T> {
private next: Handler<T> | null = null;
private middlewares: ((data: T) => T)[] = [];
setNext(handler: Handler<T>): Handler<T> {
this.next = handler;
return handler;
}
use(middleware: (data: T) => T): this {
this.middlewares.push(middleware);
return this;
}
async handle(data: T): Promise<T | null> {
// ๐ฏ Apply middlewares
let processedData = data;
for (const middleware of this.middlewares) {
processedData = middleware(processedData);
}
const result = await this.process(processedData);
if (result !== null) {
return result;
}
return this.next ? this.next.handle(data) : null;
}
protected abstract process(data: T): Promise<T | null>;
}
// ๐ฏ Chain builder with configuration
class ChainBuilder<T> {
private factory: HandlerFactory<T>;
constructor(factory: HandlerFactory<T>) {
this.factory = factory;
}
build(configs: HandlerConfig[]): Handler<T> | null {
// ๐ข Sort by priority
const sorted = configs
.filter(c => c.enabled)
.sort((a, b) => a.priority - b.priority);
if (sorted.length === 0) return null;
// ๐๏ธ Build the chain
const handlers = sorted
.map(config => this.factory.create(config.name))
.filter((h): h is Handler<T> => h !== null);
if (handlers.length === 0) return null;
// ๐ Link them together
for (let i = 0; i < handlers.length - 1; i++) {
handlers[i].setNext(handlers[i + 1]);
}
return handlers[0];
}
}
// ๐ Example: Order processing chain
interface Order {
id: string;
items: Array<{ name: string; price: number; emoji: string }>;
total: number;
status: string;
customer: {
name: string;
membership: 'bronze' | 'silver' | 'gold';
previousOrders: number;
};
}
// ๐ฐ Discount handler
class DiscountHandler extends Handler<Order> {
protected async process(order: Order): Promise<Order | null> {
if (order.customer.membership === 'gold') {
const discount = order.total * 0.2;
console.log(`๐ฐ Applied 20% gold discount: -$${discount.toFixed(2)}`);
return {
...order,
total: order.total - discount,
status: 'discounted'
};
}
return null;
}
}
// ๐ Loyalty points handler
class LoyaltyHandler extends Handler<Order> {
protected async process(order: Order): Promise<Order | null> {
if (order.customer.previousOrders >= 5) {
const points = Math.floor(order.total * 10);
console.log(`๐ Awarded ${points} loyalty points!`);
return {
...order,
status: 'loyalty_processed'
};
}
return null;
}
}
// ๐ฆ Inventory check handler
class InventoryHandler extends Handler<Order> {
private inventory = new Map([
['Pizza ๐', 10],
['Burger ๐', 15],
['Taco ๐ฎ', 20]
]);
protected async process(order: Order): Promise<Order | null> {
for (const item of order.items) {
const stock = this.inventory.get(item.name) || 0;
if (stock < 1) {
console.log(`โ Out of stock: ${item.name}`);
return {
...order,
status: 'out_of_stock'
};
}
}
console.log('โ
All items in stock!');
return null;
}
}
// ๐ Shipping handler
class ShippingHandler extends Handler<Order> {
protected async process(order: Order): Promise<Order | null> {
if (order.total > 50) {
console.log('๐ Free shipping applied!');
return {
...order,
status: 'free_shipping'
};
}
return null;
}
}
// ๐ฎ Let's use the advanced chain!
const setupOrderProcessing = () => {
// ๐ญ Setup factory
const factory = new HandlerFactory<Order>();
factory.register('discount', DiscountHandler);
factory.register('loyalty', LoyaltyHandler);
factory.register('inventory', InventoryHandler);
factory.register('shipping', ShippingHandler);
// ๐ Configuration
const config: HandlerConfig[] = [
{ name: 'inventory', priority: 1, enabled: true },
{ name: 'discount', priority: 2, enabled: true },
{ name: 'shipping', priority: 3, enabled: true },
{ name: 'loyalty', priority: 4, enabled: true }
];
// ๐๏ธ Build chain
const builder = new ChainBuilder(factory);
return builder.build(config);
};
// ๐งช Test the advanced chain
const orderChain = setupOrderProcessing();
if (orderChain) {
const testOrder: Order = {
id: 'ORD-001',
items: [
{ name: 'Pizza ๐', price: 15, emoji: '๐' },
{ name: 'Burger ๐', price: 12, emoji: '๐' },
{ name: 'Taco ๐ฎ', price: 8, emoji: '๐ฎ' }
],
total: 60,
status: 'pending',
customer: {
name: 'Alice',
membership: 'gold',
previousOrders: 10
}
};
orderChain.handle(testOrder).then(result => {
console.log('๐ Final order:', result);
});
}
โ ๏ธ Common Pitfalls and Solutions
โ Wrong: Forgetting to set next handler
// โ Broken chain - handlers not connected!
const handler1 = new BotHandler();
const handler2 = new AgentHandler();
const handler3 = new SupervisorHandler();
// ๐ฑ They're not linked!
const result = handler1.handle(ticket); // Only handler1 will work
โ Correct: Properly linking handlers
// โ
Properly linked chain
const handler1 = new BotHandler();
const handler2 = new AgentHandler();
const handler3 = new SupervisorHandler();
// ๐ Link them together
handler1.setNext(handler2).setNext(handler3);
const result = handler1.handle(ticket); // Full chain works!
โ Wrong: Circular references
// โ Infinite loop danger!
const handlerA = new HandlerA();
const handlerB = new HandlerB();
handlerA.setNext(handlerB);
handlerB.setNext(handlerA); // ๐ฑ Circular reference!
โ Correct: Linear chain with termination
// โ
Safe linear chain
const handlers: Handler[] = [
new HandlerA(),
new HandlerB(),
new HandlerC()
];
// ๐ Link in order
for (let i = 0; i < handlers.length - 1; i++) {
handlers[i].setNext(handlers[i + 1]);
}
// Last handler has no next - chain ends safely
โ Wrong: Not handling null results
// โ Assumes handler always returns something
class BadHandler extends Handler {
handle(request: Request): Response {
const result = this.process(request);
return result.data; // ๐ฅ What if result is null?
}
}
โ Correct: Proper null handling
// โ
Handles null results properly
class GoodHandler extends Handler {
handle(request: Request): Response | null {
const result = this.process(request);
if (result) {
return result;
}
// ๐ Try next handler or return default
return this.next ? this.next.handle(request) : null;
}
}
๐ ๏ธ Best Practices
-
Keep handlers focused ๐ฏ
- Each handler should have a single responsibility
- Donโt put too much logic in one handler
-
Order matters ๐
- Place more specific handlers before general ones
- Consider performance - put fast checks first
-
Make handlers configurable โ๏ธ
- Use constructor parameters for flexibility
- Allow dynamic chain building
-
Add logging ๐
- Log which handler processed the request
- Track performance metrics
-
Handle errors gracefully ๐ก๏ธ
- Wrap handler logic in try-catch
- Decide whether errors stop the chain
-
Use TypeScript features ๐
- Leverage generics for reusable handlers
- Use abstract classes for common behavior
-
Test each handler independently ๐งช
- Unit test individual handlers
- Integration test the full chain
-
Document the chain flow ๐
- Create diagrams showing handler order
- Document what each handler handles
๐งช Hands-On Exercise
Time to build your own chain! Create a notification system that sends alerts through different channels based on priority and user preferences.
Your Challenge:
- Create handlers for Email, SMS, Push, and Slack notifications
- Implement priority-based routing (critical โ all channels, high โ SMS+Email, etc.)
- Add user preference checking (some users might opt-out of certain channels)
- Include retry logic for failed notifications
๐ก Click to see the solution
// ๐ฑ Notification system implementation
interface Notification {
id: string;
message: string;
priority: 'low' | 'medium' | 'high' | 'critical';
userId: string;
timestamp: number;
channels?: string[];
}
interface UserPreferences {
userId: string;
enabledChannels: Set<string>;
quietHours?: { start: number; end: number };
}
interface NotificationResult {
sent: boolean;
channel: string;
error?: string;
}
// ๐๏ธ Base notification handler
abstract class NotificationHandler {
private next: NotificationHandler | null = null;
protected preferences = new Map<string, UserPreferences>([
['user1', {
userId: 'user1',
enabledChannels: new Set(['email', 'push', 'slack']),
quietHours: { start: 22, end: 8 }
}],
['user2', {
userId: 'user2',
enabledChannels: new Set(['email', 'sms'])
}]
]);
setNext(handler: NotificationHandler): NotificationHandler {
this.next = handler;
return handler;
}
async send(notification: Notification): Promise<NotificationResult[]> {
const results: NotificationResult[] = [];
// ๐ Check if this handler should process
if (this.shouldHandle(notification)) {
const result = await this.sendNotification(notification);
results.push(result);
// ๐ Stop if critical and sent successfully
if (notification.priority === 'critical' && result.sent) {
return results;
}
}
// ๐จ Pass to next handler
if (this.next) {
const nextResults = await this.next.send(notification);
results.push(...nextResults);
}
return results;
}
protected abstract shouldHandle(notification: Notification): boolean;
protected abstract sendNotification(notification: Notification): Promise<NotificationResult>;
protected canSendToUser(userId: string, channel: string): boolean {
const prefs = this.preferences.get(userId);
if (!prefs) return true; // Default to enabled
// ๐ Check quiet hours
if (prefs.quietHours) {
const hour = new Date().getHours();
if (hour >= prefs.quietHours.start || hour < prefs.quietHours.end) {
console.log(`๐ User ${userId} is in quiet hours`);
return false;
}
}
return prefs.enabledChannels.has(channel);
}
}
// ๐ง Email handler
class EmailHandler extends NotificationHandler {
private retryCount = 3;
protected shouldHandle(notification: Notification): boolean {
return notification.priority !== 'low' &&
this.canSendToUser(notification.userId, 'email');
}
protected async sendNotification(notification: Notification): Promise<NotificationResult> {
let attempts = 0;
while (attempts < this.retryCount) {
try {
// ๐ฏ Simulate sending email
await this.simulateSend();
console.log(`๐ง Email sent: ${notification.message}`);
return { sent: true, channel: 'email' };
} catch (error) {
attempts++;
console.log(`โ Email attempt ${attempts} failed`);
}
}
return { sent: false, channel: 'email', error: 'Max retries exceeded' };
}
private async simulateSend(): Promise<void> {
// 90% success rate
if (Math.random() > 0.9) {
throw new Error('Network timeout');
}
}
}
// ๐ฑ SMS handler
class SMSHandler extends NotificationHandler {
protected shouldHandle(notification: Notification): boolean {
return (notification.priority === 'high' || notification.priority === 'critical') &&
this.canSendToUser(notification.userId, 'sms');
}
protected async sendNotification(notification: Notification): Promise<NotificationResult> {
// ๐ Truncate message for SMS
const truncated = notification.message.substring(0, 160);
console.log(`๐ฑ SMS sent: ${truncated}`);
return { sent: true, channel: 'sms' };
}
}
// ๐ Push notification handler
class PushHandler extends NotificationHandler {
protected shouldHandle(notification: Notification): boolean {
return notification.priority !== 'low' &&
this.canSendToUser(notification.userId, 'push');
}
protected async sendNotification(notification: Notification): Promise<NotificationResult> {
console.log(`๐ Push notification sent: ${notification.message}`);
return { sent: true, channel: 'push' };
}
}
// ๐ฌ Slack handler
class SlackHandler extends NotificationHandler {
protected shouldHandle(notification: Notification): boolean {
return notification.priority === 'critical' &&
this.canSendToUser(notification.userId, 'slack');
}
protected async sendNotification(notification: Notification): Promise<NotificationResult> {
const formattedMessage = `๐จ *Critical Alert*\n${notification.message}`;
console.log(`๐ฌ Slack message sent: ${formattedMessage}`);
return { sent: true, channel: 'slack' };
}
}
// ๐๏ธ Build notification chain
const buildNotificationChain = (): NotificationHandler => {
const email = new EmailHandler();
const sms = new SMSHandler();
const push = new PushHandler();
const slack = new SlackHandler();
email.setNext(sms).setNext(push).setNext(slack);
return email;
};
// ๐งช Test the notification system
const testNotificationSystem = async () => {
const chain = buildNotificationChain();
const notifications: Notification[] = [
{
id: 'N001',
message: 'Your order has been shipped! ๐ฆ',
priority: 'low',
userId: 'user1',
timestamp: Date.now()
},
{
id: 'N002',
message: 'Payment reminder: $99 due tomorrow ๐ณ',
priority: 'high',
userId: 'user1',
timestamp: Date.now()
},
{
id: 'N003',
message: 'SECURITY ALERT: Unusual login detected! ๐จ',
priority: 'critical',
userId: 'user2',
timestamp: Date.now()
}
];
for (const notification of notifications) {
console.log(`\n๐จ Processing ${notification.priority} notification:`);
const results = await chain.send(notification);
console.log('Results:', results);
}
};
// ๐ฎ Run the test!
testNotificationSystem();
๐ Key Takeaways
Youโve mastered the Chain of Responsibility pattern! Hereโs what youโve learned:
- ๐ Chain building: Link handlers together to create processing pipelines
- ๐ฏ Single responsibility: Each handler focuses on one type of request
- โก๏ธ Request forwarding: Pass requests along until someone handles them
- ๐ก๏ธ Decoupling: Senders donโt need to know about specific handlers
- ๐ง Flexibility: Easily add, remove, or reorder handlers
- ๐ฎ Real-world uses: Support systems, authentication, validation, and more!
The Chain of Responsibility pattern is perfect when you need to:
- Process requests through multiple steps
- Allow different handlers for different scenarios
- Build flexible, extensible systems
- Decouple request senders from handlers
๐ค Next Steps
Congratulations! ๐ Youโve built some amazing chain-based systems! Your request handling skills are now top-notch!
To continue your journey:
- Try combining chains with other patterns (like Strategy or Observer)
- Build a middleware system for a web framework
- Create a plugin architecture using chains
- Implement an event processing pipeline
Keep building those chains, and remember - every great system is just a series of well-connected handlers! Youโre doing fantastic! ๐
Happy coding! ๐โจ