Loading learning content...
The Decorator pattern isn't abstract theory—it's embedded throughout production software. From the Java I/O library to Express.js middleware to React higher-order components, decoration is ubiquitous. Understanding these real-world applications solidifies your grasp of when and how to apply the pattern.
This page surveys major domains where decoration excels, providing concrete examples you can adapt to your own projects.
By the end of this page, you will recognize the Decorator pattern in I/O systems, web frameworks, UI libraries, and data access layers. You'll have a library of practical examples demonstrating how decoration solves real problems across diverse domains.
The Java I/O library is the canonical example of the Decorator pattern. When Java was designed, its I/O system used decorators to avoid the class explosion problem we discussed earlier. The same pattern appears in .NET, Python, and other platforms.
The Java I/O structure:
InputStream / OutputStream — Component interfacesFileInputStream / FileOutputStream — Concrete components (actual I/O)FilterInputStream / FilterOutputStream — Base decoratorsBufferedInputStream, GZIPInputStream, CipherInputStream — Concrete decorators12345678910111213141516171819202122232425262728293031323334353637383940414243
// Classic Java I/O example showing decorator composition// Each layer adds one responsibility import java.io.*;import java.util.zip.*;import javax.crypto.*; // Reading a compressed, encrypted file with bufferingpublic FileReader createSecureReader(String path, Cipher cipher) { // Build from inside out: File → Decrypt → Decompress → Buffer InputStream file = new FileInputStream(path); InputStream decrypted = new CipherInputStream(file, cipher); InputStream decompressed = new GZIPInputStream(decrypted); InputStream buffered = new BufferedInputStream(decompressed); return new InputStreamReader(buffered, StandardCharsets.UTF_8);} // Same approach for writingpublic FileWriter createSecureWriter(String path, Cipher cipher) { // Build from inside out: File → Encrypt → Compress → Buffer OutputStream file = new FileOutputStream(path); OutputStream encrypted = new CipherOutputStream(file, cipher); OutputStream compressed = new GZIPOutputStream(encrypted); OutputStream buffered = new BufferedOutputStream(compressed); return new OutputStreamWriter(buffered, StandardCharsets.UTF_8);} // Fluent alternative using try-with-resourcestry (var writer = new BufferedWriter( new OutputStreamWriter( new GZIPOutputStream( new CipherOutputStream( new FileOutputStream("data.enc.gz"), cipher ) ), StandardCharsets.UTF_8 ) )) { writer.write("Secure compressed data");}TypeScript/Node.js equivalent:
Node.js streams follow the same pattern, though with a slightly different API:
1234567891011121314151617181920212223242526272829303132333435363738
import { createReadStream, createWriteStream } from 'fs';import { createGzip, createGunzip } from 'zlib';import { createCipheriv, createDecipheriv } from 'crypto';import { pipeline } from 'stream/promises'; // Writing: File → Cipher → Gzip (compression after encryption)async function writeSecure(inputPath: string, outputPath: string): Promise<void> { const cipher = createCipheriv('aes-256-cbc', key, iv); const gzip = createGzip(); await pipeline( createReadStream(inputPath), cipher, // Decorator 1: encryption gzip, // Decorator 2: compression createWriteStream(outputPath) );} // Reading: Gunzip → Decipher → Consumer (reverse order)async function readSecure(inputPath: string): Promise<Buffer> { const decipher = createDecipheriv('aes-256-cbc', key, iv); const gunzip = createGunzip(); const chunks: Buffer[] = []; await pipeline( createReadStream(inputPath), gunzip, // Unwrap compression first decipher, // Then decrypt async function* (source) { for await (const chunk of source) { chunks.push(chunk); } } ); return Buffer.concat(chunks);}For optimal efficiency, compress BEFORE encrypting when writing (crypto outputs high-entropy data that doesn't compress well). When reading, you must reverse the order: decrypt before decompressing. The decorator pattern makes this order explicit and controllable.
Web frameworks like Express.js, Koa, ASP.NET Core, and Django use middleware—which is essentially the Decorator pattern applied to HTTP request handling. Each middleware layer wraps the next, adding behavior before/after the core handler.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
import express, { Request, Response, NextFunction } from 'express'; const app = express(); // Each middleware is a decorator around the next handler// Order of app.use() determines decorator nesting // Decorator 1: Request loggingapp.use((req: Request, res: Response, next: NextFunction) => { console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`); const start = Date.now(); res.on('finish', () => { console.log(`[${new Date().toISOString()}] ${res.statusCode} in ${Date.now() - start}ms`); }); next(); // Delegate to wrapped handler}); // Decorator 2: Authenticationapp.use((req: Request, res: Response, next: NextFunction) => { const token = req.headers.authorization?.replace('Bearer ', ''); if (!validateToken(token)) { return res.status(401).json({ error: 'Unauthorized' }); } req.user = decodeToken(token); next(); // Delegate if authenticated}); // Decorator 3: Rate limitingapp.use((req: Request, res: Response, next: NextFunction) => { const userKey = req.user?.id || req.ip; if (isRateLimited(userKey)) { return res.status(429).json({ error: 'Too many requests' }); } incrementRequestCount(userKey); next();}); // Decorator 4: Error handling (wraps everything)app.use((err: Error, req: Request, res: Response, next: NextFunction) => { console.error('Unhandled error:', err); res.status(500).json({ error: 'Internal server error' });}); // The actual handler (wrapped by all middleware above)app.get('/api/users/:id', async (req, res) => { const user = await findUser(req.params.id); res.json(user);}); // Request flow:// Request → Logging → Auth → RateLimit → Handler → Response// Each middleware decorates the next layer in the chain123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
// Making the Decorator pattern explicit in a custom framework interface HttpHandler { handle(request: HttpRequest): Promise<HttpResponse>;} // Base handler (ConcreteComponent)class UserController implements HttpHandler { async handle(request: HttpRequest): Promise<HttpResponse> { const user = await this.userService.findById(request.params.id); return HttpResponse.ok(user); }} // Abstract decoratorabstract class HttpHandlerDecorator implements HttpHandler { constructor(protected wrapped: HttpHandler) {} async handle(request: HttpRequest): Promise<HttpResponse> { return this.wrapped.handle(request); }} // Concrete decoratorsclass LoggingDecorator extends HttpHandlerDecorator { async handle(request: HttpRequest): Promise<HttpResponse> { console.log(`Incoming: ${request.method} ${request.path}`); const start = Date.now(); const response = await super.handle(request); console.log(`Outgoing: ${response.status} in ${Date.now() - start}ms`); return response; }} class AuthDecorator extends HttpHandlerDecorator { constructor(wrapped: HttpHandler, private authService: AuthService) { super(wrapped); } async handle(request: HttpRequest): Promise<HttpResponse> { const user = await this.authService.authenticate(request); if (!user) { return HttpResponse.unauthorized(); } request.user = user; return super.handle(request); }} class RateLimitDecorator extends HttpHandlerDecorator { constructor(wrapped: HttpHandler, private limiter: RateLimiter) { super(wrapped); } async handle(request: HttpRequest): Promise<HttpResponse> { const allowed = await this.limiter.check(request.clientId); if (!allowed) { return HttpResponse.tooManyRequests(); } return super.handle(request); }} // Compose the handler with decoratorsconst userHandler = new LoggingDecorator( new AuthDecorator( new RateLimitDecorator( new UserController(), rateLimiter ), authService )); // All requests go through: Logging → Auth → RateLimit → ControllerWhen you see middleware in web frameworks—whether Express, Koa, Fastify, or ASP.NET Core—you're seeing the Decorator pattern. The next() function is the delegation to the wrapped component. Understanding this connection helps you design middleware more intentionally.
In React, Higher-Order Components (HOCs) are the Decorator pattern applied to components. A HOC is a function that takes a component and returns a new component with enhanced behavior—exactly the decorator concept applied to the UI domain.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
import React, { ComponentType, useState, useEffect } from 'react'; // Type for any component with specific propstype ComponentWithUser = { user: User }; // HOC Decorator 1: withAuthentication// Wraps a component to require authenticationfunction withAuthentication<P extends ComponentWithUser>( WrappedComponent: ComponentType<P>): ComponentType<Omit<P, 'user'>> { return function AuthenticatedComponent(props: Omit<P, 'user'>) { const [user, setUser] = useState<User | null>(null); const [loading, setLoading] = useState(true); useEffect(() => { authService.getCurrentUser() .then(setUser) .finally(() => setLoading(false)); }, []); if (loading) return <LoadingSpinner />; if (!user) return <LoginRedirect />; // Inject user prop and render wrapped component return <WrappedComponent {...props as P} user={user} />; };} // HOC Decorator 2: withLogging// Wraps a component to log rendersfunction withLogging<P extends object>( WrappedComponent: ComponentType<P>, componentName: string): ComponentType<P> { return function LoggedComponent(props: P) { useEffect(() => { console.log(`[${componentName}] Mounted`); return () => console.log(`[${componentName}] Unmounted`); }, []); console.log(`[${componentName}] Rendering with props:`, props); return <WrappedComponent {...props} />; };} // HOC Decorator 3: withErrorBoundaryfunction withErrorBoundary<P extends object>( WrappedComponent: ComponentType<P>, FallbackComponent: ComponentType<{ error: Error }>): ComponentType<P> { return class ErrorBoundary extends React.Component<P, { error: Error | null }> { state = { error: null }; static getDerivedStateFromError(error: Error) { return { error }; } render() { if (this.state.error) { return <FallbackComponent error={this.state.error} />; } return <WrappedComponent {...this.props} />; } };} // The base component (ConcreteComponent)function UserProfile({ user }: { user: User }) { return ( <div> <h1>{user.name}</h1> <p>{user.email}</p> </div> );} // Compose decorators (order: inner → outer)const EnhancedUserProfile = withErrorBoundary( withLogging( withAuthentication(UserProfile), 'UserProfile' ), ErrorFallback); // Usage: Enhanced component has all behaviorsfunction App() { return <EnhancedUserProfile />; // Renders: ErrorBoundary → Logging → Authentication → UserProfile}Modern alternative: Hooks with render props
While HOCs are a pure Decorator implementation, modern React often achieves similar composition through hooks and render props. However, understanding HOCs as decorators helps you recognize the pattern across paradigms:
12345678910111213141516171819202122232425262728293031323334353637383940
// Using hooks achieves similar results with different syntax// The compositional idea remains the same function useAuth(): { user: User | null; loading: boolean } { const [user, setUser] = useState<User | null>(null); const [loading, setLoading] = useState(true); useEffect(() => { authService.getCurrentUser() .then(setUser) .finally(() => setLoading(false)); }, []); return { user, loading };} function useLogging(componentName: string): void { useEffect(() => { console.log(`[${componentName}] Mounted`); return () => console.log(`[${componentName}] Unmounted`); }, []);} // Component composes behaviors via hooksfunction UserProfile() { useLogging('UserProfile'); const { user, loading } = useAuth(); if (loading) return <LoadingSpinner />; if (!user) return <LoginRedirect />; return ( <ErrorBoundary fallback={<ErrorFallback />}> <div> <h1>{user.name}</h1> <p>{user.email}</p> </div> </ErrorBoundary> );}One of the most practical applications of the Decorator pattern is enhancing data access layers. Caching, logging, retry logic, and circuit breakers can all be added as decorators around repositories without modifying the core data access code.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
// Component interfaceinterface UserRepository { findById(id: string): Promise<User | null>; findByEmail(email: string): Promise<User | null>; save(user: User): Promise<void>; delete(id: string): Promise<void>;} // ConcreteComponent: Actual database accessclass PostgresUserRepository implements UserRepository { constructor(private pool: Pool) {} async findById(id: string): Promise<User | null> { const result = await this.pool.query( 'SELECT * FROM users WHERE id = $1', [id] ); return result.rows[0] ?? null; } async findByEmail(email: string): Promise<User | null> { const result = await this.pool.query( 'SELECT * FROM users WHERE email = $1', [email] ); return result.rows[0] ?? null; } async save(user: User): Promise<void> { await this.pool.query( `INSERT INTO users (id, name, email) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET name = $2, email = $3`, [user.id, user.name, user.email] ); } async delete(id: string): Promise<void> { await this.pool.query('DELETE FROM users WHERE id = $1', [id]); }} // Base decoratorabstract class UserRepositoryDecorator implements UserRepository { constructor(protected wrapped: UserRepository) {} findById(id: string): Promise<User | null> { return this.wrapped.findById(id); } findByEmail(email: string): Promise<User | null> { return this.wrapped.findByEmail(email); } save(user: User): Promise<void> { return this.wrapped.save(user); } delete(id: string): Promise<void> { return this.wrapped.delete(id); }} // Decorator 1: Cachingclass CachingUserRepository extends UserRepositoryDecorator { constructor(wrapped: UserRepository, private cache: Cache) { super(wrapped); } async findById(id: string): Promise<User | null> { const cached = await this.cache.get<User>(`user:${id}`); if (cached) { console.log(`[Cache] HIT for user:${id}`); return cached; } console.log(`[Cache] MISS for user:${id}`); const user = await this.wrapped.findById(id); if (user) { await this.cache.set(`user:${id}`, user, { ttl: 3600 }); } return user; } async save(user: User): Promise<void> { await this.wrapped.save(user); // Invalidate cache on write await this.cache.delete(`user:${user.id}`); await this.cache.delete(`user:email:${user.email}`); } async delete(id: string): Promise<void> { const user = await this.wrapped.findById(id); await this.wrapped.delete(id); if (user) { await this.cache.delete(`user:${id}`); await this.cache.delete(`user:email:${user.email}`); } }} // Decorator 2: Retry Logicclass RetryingUserRepository extends UserRepositoryDecorator { constructor( wrapped: UserRepository, private maxRetries: number = 3, private delayMs: number = 1000 ) { super(wrapped); } private async withRetry<T>(operation: () => Promise<T>): Promise<T> { let lastError: Error; for (let attempt = 1; attempt <= this.maxRetries; attempt++) { try { return await operation(); } catch (error) { lastError = error as Error; console.log(`[Retry] Attempt ${attempt} failed: ${lastError.message}`); if (attempt < this.maxRetries) { await this.delay(this.delayMs * attempt); } } } throw lastError!; } private delay(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } findById(id: string): Promise<User | null> { return this.withRetry(() => this.wrapped.findById(id)); } findByEmail(email: string): Promise<User | null> { return this.withRetry(() => this.wrapped.findByEmail(email)); } save(user: User): Promise<void> { return this.withRetry(() => this.wrapped.save(user)); } delete(id: string): Promise<void> { return this.withRetry(() => this.wrapped.delete(id)); }} // Decorator 3: Metricsclass MetricsUserRepository extends UserRepositoryDecorator { constructor(wrapped: UserRepository, private metrics: MetricsClient) { super(wrapped); } private async withMetrics<T>( operation: string, fn: () => Promise<T> ): Promise<T> { const timer = this.metrics.startTimer('repository_operation', { repository: 'user', operation }); try { const result = await fn(); this.metrics.increment('repository_success', { operation }); return result; } catch (error) { this.metrics.increment('repository_error', { operation }); throw error; } finally { timer.end(); } } findById(id: string): Promise<User | null> { return this.withMetrics('findById', () => this.wrapped.findById(id)); } // ... other methods with metrics}123456789101112131415161718192021222324252627282930313233343536
// Factory function that builds decorated repositoryfunction createUserRepository(config: AppConfig): UserRepository { // Start with concrete implementation let repository: UserRepository = new PostgresUserRepository(dbPool); // Add retry logic first (closest to DB, retries raw operations) if (config.database.enableRetry) { repository = new RetryingUserRepository( repository, config.database.maxRetries, config.database.retryDelay ); } // Add metrics (measures operation including retries) if (config.observability.enableMetrics) { repository = new MetricsUserRepository(repository, metricsClient); } // Add caching (outermost, avoids hitting DB for cached data) if (config.caching.enabled) { repository = new CachingUserRepository(repository, cache); } return repository;} // Usageconst userRepo = createUserRepository(appConfig); // Call goes through: Cache → Metrics → Retry → Postgresconst user = await userRepo.findById('user-123'); // If cached: Cache HIT → returns immediately (Metrics and Retry not touched)// If not cached: Cache MISS → Metrics starts → Retry wraps → Postgres query// → Retry succeeds → Metrics records → Cache stores → returnsHTTP clients are another excellent decorator use case. Logging, retries, circuit breakers, authentication, and caching can all layer onto a base HTTP client without modifying it:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
interface HttpClient { request<T>(config: RequestConfig): Promise<HttpResponse<T>>;} interface RequestConfig { method: 'GET' | 'POST' | 'PUT' | 'DELETE'; url: string; data?: unknown; headers?: Record<string, string>;} interface HttpResponse<T> { status: number; data: T; headers: Record<string, string>;} // ConcreteComponent: Actual HTTP implementationclass FetchHttpClient implements HttpClient { async request<T>(config: RequestConfig): Promise<HttpResponse<T>> { const response = await fetch(config.url, { method: config.method, headers: config.headers, body: config.data ? JSON.stringify(config.data) : undefined, }); return { status: response.status, data: await response.json(), headers: Object.fromEntries(response.headers), }; }} // Decorator: Authenticationclass AuthenticatedHttpClient implements HttpClient { constructor( private wrapped: HttpClient, private tokenProvider: () => Promise<string> ) {} async request<T>(config: RequestConfig): Promise<HttpResponse<T>> { const token = await this.tokenProvider(); return this.wrapped.request({ ...config, headers: { ...config.headers, Authorization: `Bearer ${token}`, }, }); }} // Decorator: Retry with exponential backoffclass RetryHttpClient implements HttpClient { constructor( private wrapped: HttpClient, private maxRetries: number = 3 ) {} async request<T>(config: RequestConfig): Promise<HttpResponse<T>> { let lastError: Error; for (let attempt = 0; attempt < this.maxRetries; attempt++) { try { const response = await this.wrapped.request<T>(config); // Retry on 5xx errors if (response.status >= 500) { throw new Error(`Server error: ${response.status}`); } return response; } catch (error) { lastError = error as Error; if (attempt < this.maxRetries - 1) { const delay = Math.pow(2, attempt) * 1000; await new Promise(r => setTimeout(r, delay)); } } } throw lastError!; }} // Decorator: Circuit Breakerclass CircuitBreakerHttpClient implements HttpClient { private failureCount = 0; private lastFailureTime = 0; private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED'; constructor( private wrapped: HttpClient, private threshold: number = 5, private resetTimeMs: number = 30000 ) {} async request<T>(config: RequestConfig): Promise<HttpResponse<T>> { if (this.state === 'OPEN') { if (Date.now() - this.lastFailureTime > this.resetTimeMs) { this.state = 'HALF_OPEN'; } else { throw new Error('Circuit breaker is OPEN'); } } try { const response = await this.wrapped.request<T>(config); this.onSuccess(); return response; } catch (error) { this.onFailure(); throw error; } } private onSuccess(): void { this.failureCount = 0; this.state = 'CLOSED'; } private onFailure(): void { this.failureCount++; this.lastFailureTime = Date.now(); if (this.failureCount >= this.threshold) { this.state = 'OPEN'; } }} // Decorator: Request/Response Loggingclass LoggingHttpClient implements HttpClient { constructor( private wrapped: HttpClient, private logger: Logger ) {} async request<T>(config: RequestConfig): Promise<HttpResponse<T>> { const requestId = crypto.randomUUID(); this.logger.info({ requestId, type: 'HTTP_REQUEST', method: config.method, url: config.url, }); const start = Date.now(); try { const response = await this.wrapped.request<T>(config); this.logger.info({ requestId, type: 'HTTP_RESPONSE', status: response.status, durationMs: Date.now() - start, }); return response; } catch (error) { this.logger.error({ requestId, type: 'HTTP_ERROR', error: (error as Error).message, durationMs: Date.now() - start, }); throw error; } }} // Compose into production-ready clientconst apiClient = new LoggingHttpClient( new CircuitBreakerHttpClient( new RetryHttpClient( new AuthenticatedHttpClient( new FetchHttpClient(), () => authService.getAccessToken() ) ) ), logger); // Order: Logging → CircuitBreaker → Retry → Auth → FetchInput validation is another natural fit for decoration. Each validator checks one aspect of the input, and validators chain together to form complete validation pipelines:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
// Component interfaceinterface Validator<T> { validate(input: T): ValidationResult;} interface ValidationResult { valid: boolean; errors: string[];} // Base decorator that chains validatorsabstract class ValidatorDecorator<T> implements Validator<T> { constructor(protected next: Validator<T> | null = null) {} validate(input: T): ValidationResult { const currentResult = this.doValidate(input); // If invalid, can stop or continue (configurable) if (!currentResult.valid) { return currentResult; } // Chain to next validator if present if (this.next) { const nextResult = this.next.validate(input); return { valid: nextResult.valid, errors: [...currentResult.errors, ...nextResult.errors], }; } return currentResult; } protected abstract doValidate(input: T): ValidationResult;} // Concrete validators for user registrationinterface RegistrationData { email: string; password: string; username: string; age: number;} class EmailValidator extends ValidatorDecorator<RegistrationData> { private emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; protected doValidate(input: RegistrationData): ValidationResult { if (!this.emailRegex.test(input.email)) { return { valid: false, errors: ['Invalid email format'] }; } return { valid: true, errors: [] }; }} class PasswordStrengthValidator extends ValidatorDecorator<RegistrationData> { protected doValidate(input: RegistrationData): ValidationResult { const errors: string[] = []; if (input.password.length < 8) { errors.push('Password must be at least 8 characters'); } if (!/[A-Z]/.test(input.password)) { errors.push('Password must contain uppercase letter'); } if (!/[a-z]/.test(input.password)) { errors.push('Password must contain lowercase letter'); } if (!/[0-9]/.test(input.password)) { errors.push('Password must contain a number'); } return { valid: errors.length === 0, errors }; }} class UsernameValidator extends ValidatorDecorator<RegistrationData> { protected doValidate(input: RegistrationData): ValidationResult { if (input.username.length < 3) { return { valid: false, errors: ['Username must be at least 3 characters'] }; } if (!/^[a-zA-Z0-9_]+$/.test(input.username)) { return { valid: false, errors: ['Username can only contain letters, numbers, and underscores'] }; } return { valid: true, errors: [] }; }} class AgeValidator extends ValidatorDecorator<RegistrationData> { protected doValidate(input: RegistrationData): ValidationResult { if (input.age < 13) { return { valid: false, errors: ['Must be at least 13 years old'] }; } if (input.age > 120) { return { valid: false, errors: ['Invalid age provided'] }; } return { valid: true, errors: [] }; }} // Compose validator chainconst registrationValidator = new EmailValidator( new PasswordStrengthValidator( new UsernameValidator( new AgeValidator() ) )); // Usageconst result = registrationValidator.validate({ email: 'invalid-email', password: 'weak', username: 'ab', age: 10,}); // Result: { valid: false, errors: ['Invalid email format'] }// Stops at first failure; change to collect all errors by modifying base decoratorText transformations layer naturally as decorators. Sanitization, formatting, encoding, and templating can all compose:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
interface TextProcessor { process(text: string): string;} // ConcreteComponent: Identity processor (no-op)class IdentityProcessor implements TextProcessor { process(text: string): string { return text; }} // Base decoratorabstract class TextProcessorDecorator implements TextProcessor { constructor(protected wrapped: TextProcessor) {} process(text: string): string { return this.wrapped.process(text); }} // Decorator: HTML entity encoding (XSS prevention)class HtmlEncoder extends TextProcessorDecorator { private entities: Record<string, string> = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }; process(text: string): string { const encoded = text.replace( /[&<>"']/g, char => this.entities[char] ); return super.process(encoded); }} // Decorator: Markdown to HTMLclass MarkdownProcessor extends TextProcessorDecorator { process(text: string): string { let html = text .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>') .replace(/\*(.+?)\*/g, '<em>$1</em>') .replace(/\n/g, '<br>'); return super.process(html); }} // Decorator: Profanity filterclass ProfanityFilter extends TextProcessorDecorator { private badWords = ['badword1', 'badword2']; // Simplified process(text: string): string { let filtered = text; for (const word of this.badWords) { const regex = new RegExp(word, 'gi'); filtered = filtered.replace(regex, '*'.repeat(word.length)); } return super.process(filtered); }} // Decorator: Link detectionclass LinkDetector extends TextProcessorDecorator { private urlRegex = /(https?:\/\/[^\s]+)/g; process(text: string): string { const linked = text.replace( this.urlRegex, '<a href="$1" target="_blank">$1</a>' ); return super.process(linked); }} // Decorator: Emoji conversionclass EmojiConverter extends TextProcessorDecorator { private emojiMap: Record<string, string> = { ':)': '😊', ':(': '😢', ':D': '😄', '<3': '❤️', }; process(text: string): string { let converted = text; for (const [code, emoji] of Object.entries(this.emojiMap)) { converted = converted.split(code).join(emoji); } return super.process(converted); }} // Compose for different use casesconst userBioProcessor = new HtmlEncoder( new LinkDetector( new ProfanityFilter( new EmojiConverter( new IdentityProcessor() ) ) )); const commentProcessor = new HtmlEncoder( new MarkdownProcessor( new ProfanityFilter( new EmojiConverter( new IdentityProcessor() ) ) )); // Usageconst bio = userBioProcessor.process( 'Check out my site: https://example.com :) <script>alert("xss")</script>');// Result: Emojis converted, link wrapped, HTML encoded, no XSSLet's consolidate the patterns and best practices from the examples we've explored:
| Domain | Example Use Cases | Key Benefits |
|---|---|---|
| I/O Streams | Buffering, compression, encryption, checksumming | Compose stream processing in any order at runtime |
| Web Middleware | Logging, auth, rate limiting, CORS, compression | Pipeline of independent, testable request processors |
| UI Components | HOCs for auth, logging, error boundaries, theming | Cross-cutting UI behavior without prop drilling |
| Data Access | Caching, retries, metrics, circuit breakers | Layer reliability features without modifying repositories |
| HTTP Clients | Auth, retry, circuit breaker, logging, caching | Resilient clients from simple building blocks |
| Validation | Email, password, format, business rule validators | Composable validation pipelines |
| Text Processing | Sanitization, encoding, formatting, filtering | Safe, layered text transformations |
You've completed the Decorator Pattern module. You now understand the problem of adding behavior dynamically, the solution of wrapping with decorator layers, when to prefer decoration over inheritance, and how the pattern appears across diverse domains—from streams to middleware to UI components. Apply these patterns to create flexible, maintainable systems that extend gracefully.