Loading learning content...
Textbook examples of design patterns tend to be pristine—neatly bounded problems with obvious solutions. Real production systems are messier. Requirements are ambiguous, constraints conflict, and optimal solutions must balance competing concerns. The true test of pattern mastery isn't recognizing StreamDecorator in a tutorial; it's recognizing when your user import system needs an Adapter, why your notification service would benefit from Bridge, or how to design your rendering engine with Flyweight.
This page presents case studies from production-grade systems—the kind you'll encounter in your career. Each case study walks through the problem domain, the forces at play, the pattern selection reasoning, and the implementation with production considerations. These aren't toy examples; they're the patterns behind systems serving millions of users.
By the end of this page, you will have seen structural patterns applied to realistic, complex problems. You'll understand not just what patterns to use, but how to reason about pattern selection when requirements are fuzzy and constraints are real. This is the capstone of structural pattern education.
Context:
A growing SaaS company has dozens of microservices, each with its own API. Frontend applications shouldn't need to know about this internal complexity. The company needs an API Gateway that:
Forces Analysis:
| Force | Implication |
|---|---|
| Unified interface for complex backend | Facade needed |
| Backend services have different APIs | Adapter per service |
| Cross-cutting concerns (auth, logging, rate limiting) | Decorator/Proxy for layered concerns |
| Response aggregation | Composite for multi-service requests |
| Performance (caching) | Proxy with caching |
| Security (rate limiting, auth) | Proxy for access control |
Pattern Selection:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
// ============================================// Core Abstraction: Service Client Interface// ============================================interface ServiceClient<TRequest, TResponse> { call(request: TRequest): Promise<TResponse>;} // ============================================// ADAPTER: Normalize microservice interfaces// ============================================// User Service has RESTful APIclass UserServiceAdapter implements ServiceClient<UserRequest, User> { constructor(private httpClient: HttpClient, private baseUrl: string) {} async call(request: UserRequest): Promise<User> { const response = await this.httpClient.get( `${this.baseUrl}/users/${request.userId}`, { headers: { 'X-Internal-Token': this.getServiceToken() } } ); // Normalize response to internal User type return { id: response.data.user_id, // snake_case to camelCase email: response.data.email_address, name: response.data.full_name, createdAt: new Date(response.data.created_timestamp), }; } private getServiceToken(): string { return process.env.USER_SERVICE_TOKEN!; }} // Order Service uses GraphQLclass OrderServiceAdapter implements ServiceClient<OrdersRequest, Order[]> { constructor(private graphqlClient: GraphQLClient, private endpoint: string) {} async call(request: OrdersRequest): Promise<Order[]> { const query = ` query GetUserOrders($userId: ID!, $limit: Int) { orders(userId: $userId, first: $limit) { nodes { id, total, status, createdAt } } } `; const result = await this.graphqlClient.request(this.endpoint, query, { userId: request.userId, limit: request.limit, }); // Normalize GraphQL response return result.orders.nodes.map((o: any) => ({ id: o.id, total: parseFloat(o.total), status: this.mapStatus(o.status), createdAt: new Date(o.createdAt), })); } private mapStatus(graphqlStatus: string): OrderStatus { const mapping: Record<string, OrderStatus> = { 'PENDING_PAYMENT': OrderStatus.Pending, 'PROCESSING': OrderStatus.Processing, 'SHIPPED': OrderStatus.Shipped, 'DELIVERED': OrderStatus.Delivered, }; return mapping[graphqlStatus] || OrderStatus.Unknown; }} // ============================================// PROXY: Cross-cutting concerns// ============================================// Authentication Proxyclass AuthenticatedServiceProxy<TReq, TRes> implements ServiceClient<TReq & { authToken?: string }, TRes> { constructor( private service: ServiceClient<TReq, TRes>, private authService: AuthService ) {} async call(request: TReq & { authToken?: string }): Promise<TRes> { const { authToken, ...serviceRequest } = request; if (!authToken) { throw new UnauthorizedError('Authentication required'); } const session = await this.authService.validateToken(authToken); if (!session.valid) { throw new UnauthorizedError('Invalid or expired token'); } // Attach user context for downstream services return this.service.call({ ...serviceRequest as TReq, _context: { userId: session.userId, roles: session.roles }, }); }} // Rate Limiting Proxyclass RateLimitedServiceProxy<TReq, TRes> implements ServiceClient<TReq, TRes> { private requestCounts = new Map<string, { count: number; resetAt: number }>(); constructor( private service: ServiceClient<TReq, TRes>, private getClientId: (request: TReq) => string, private limit: number, private windowMs: number ) {} async call(request: TReq): Promise<TRes> { const clientId = this.getClientId(request); const now = Date.now(); let bucket = this.requestCounts.get(clientId); if (!bucket || bucket.resetAt < now) { bucket = { count: 0, resetAt: now + this.windowMs }; this.requestCounts.set(clientId, bucket); } if (bucket.count >= this.limit) { throw new RateLimitExceededError( `Rate limit exceeded. Retry after ${bucket.resetAt - now}ms` ); } bucket.count++; return this.service.call(request); }} // Caching Proxyclass CachedServiceProxy<TReq, TRes> implements ServiceClient<TReq, TRes> { constructor( private service: ServiceClient<TReq, TRes>, private cache: Cache, private getCacheKey: (request: TReq) => string, private ttlSeconds: number ) {} async call(request: TReq): Promise<TRes> { const key = this.getCacheKey(request); const cached = await this.cache.get<TRes>(key); if (cached !== undefined) { return cached; } const result = await this.service.call(request); await this.cache.set(key, result, this.ttlSeconds); return result; }} // ============================================// DECORATOR: Logging// ============================================class LoggingServiceDecorator<TReq, TRes> implements ServiceClient<TReq, TRes> { constructor( private service: ServiceClient<TReq, TRes>, private logger: Logger, private serviceName: string ) {} async call(request: TReq): Promise<TRes> { const startTime = Date.now(); const requestId = generateRequestId(); this.logger.info({ type: 'service_call_start', requestId, service: this.serviceName, request: this.sanitize(request), }); try { const result = await this.service.call(request); this.logger.info({ type: 'service_call_success', requestId, service: this.serviceName, durationMs: Date.now() - startTime, }); return result; } catch (error) { this.logger.error({ type: 'service_call_error', requestId, service: this.serviceName, error: error.message, durationMs: Date.now() - startTime, }); throw error; } } private sanitize(request: TReq): Partial<TReq> { // Remove sensitive fields from logs const { authToken, password, ...safe } = request as any; return safe; }} // ============================================// COMPOSITE: Aggregated responses// ============================================interface UserProfile { user: User; recentOrders: Order[]; recommendations: Product[];} class UserProfileAggregator implements ServiceClient<UserProfileRequest, UserProfile> { constructor( private userService: ServiceClient<UserRequest, User>, private orderService: ServiceClient<OrdersRequest, Order[]>, private recommendationService: ServiceClient<RecommendationRequest, Product[]> ) {} async call(request: UserProfileRequest): Promise<UserProfile> { // Parallel requests to all services const [user, recentOrders, recommendations] = await Promise.all([ this.userService.call({ userId: request.userId }), this.orderService.call({ userId: request.userId, limit: 5 }), this.recommendationService.call({ userId: request.userId, count: 10 }), ]); return { user, recentOrders, recommendations }; }} // ============================================// FACADE: API Gateway// ============================================class APIGateway { private userService: ServiceClient<UserRequest, User>; private ordersService: ServiceClient<OrdersRequest, Order[]>; private profileAggregator: ServiceClient<UserProfileRequest, UserProfile>; constructor(config: GatewayConfig) { // Build user service with all concerns layered let userSvc: ServiceClient<any, User> = new UserServiceAdapter( config.httpClient, config.serviceUrls.users ); userSvc = new CachedServiceProxy(userSvc, config.cache, r => `user:${r.userId}`, 300); userSvc = new AuthenticatedServiceProxy(userSvc, config.authService); userSvc = new RateLimitedServiceProxy(userSvc, r => r.authToken!, 100, 60000); userSvc = new LoggingServiceDecorator(userSvc, config.logger, 'UserService'); this.userService = userSvc; // Build orders service (different cache policy, stricter rate limit) let ordersSvc: ServiceClient<any, Order[]> = new OrderServiceAdapter( config.graphqlClient, config.serviceUrls.orders ); ordersSvc = new CachedServiceProxy(ordersSvc, config.cache, r => `orders:${r.userId}`, 60); ordersSvc = new AuthenticatedServiceProxy(ordersSvc, config.authService); ordersSvc = new RateLimitedServiceProxy(ordersSvc, r => r.authToken!, 50, 60000); ordersSvc = new LoggingServiceDecorator(ordersSvc, config.logger, 'OrderService'); this.ordersService = ordersSvc; // Compose aggregator from individual services this.profileAggregator = new LoggingServiceDecorator( new UserProfileAggregator( this.userService, this.ordersService, this.buildRecommendationService(config) ), config.logger, 'ProfileAggregator' ); } // Facade methods - simple interface for clients async getUser(userId: string, authToken: string): Promise<User> { return this.userService.call({ userId, authToken }); } async getUserOrders(userId: string, authToken: string, limit = 10): Promise<Order[]> { return this.ordersService.call({ userId, limit, authToken }); } async getUserProfile(userId: string, authToken: string): Promise<UserProfile> { return this.profileAggregator.call({ userId, authToken }); }}This example demonstrates key production patterns: • Circuit breaker could be added as another Proxy layer for resilience • Retry logic with exponential backoff for transient failures • Metrics collection using the Decorator pattern • Feature flags to conditionally apply Decorators • Health checks through the Facade interface
Context:
A legal technology company processes millions of documents for analysis. Documents come in various formats (PDF, Word, scanned images, emails). The system must:
Forces Analysis:
| Force | Pattern Implication |
|---|---|
| Multiple input formats with different parsing needs | Adapter for each format |
| Format × Processing Mode (batch/realtime) variations | Bridge to avoid explosion |
| Optional, combinable processing steps | Decorator for stackable operations |
| Large documents with repeating elements (pages, fonts) | Flyweight for memory |
| Common interface for all document operations | Facade for unified access |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
// ============================================// Core Domain Model// ============================================interface Document { id: string; pages: Page[]; metadata: DocumentMetadata;} interface Page { number: number; text: string; elements: PageElement[];} interface PageElement { type: 'text' | 'image' | 'table'; bounds: Rectangle; style: TextStyle; content: string;} // ============================================// FLYWEIGHT: Shared text styles// ============================================// In a 1000-page document, there might be only 10-20 unique stylesclass TextStyle { constructor( public readonly fontFamily: string, public readonly fontSize: number, public readonly color: string, public readonly bold: boolean, public readonly italic: boolean ) { // Styles are immutable - safe to share Object.freeze(this); } matches(other: TextStyle): boolean { return this.fontFamily === other.fontFamily && this.fontSize === other.fontSize && this.color === other.color && this.bold === other.bold && this.italic === other.italic; }} class TextStyleFactory { private styles = new Map<string, TextStyle>(); private static instance: TextStyleFactory; static getInstance(): TextStyleFactory { if (!this.instance) { this.instance = new TextStyleFactory(); } return this.instance; } getStyle( fontFamily: string, fontSize: number, color: string, bold: boolean, italic: boolean ): TextStyle { const key = `${fontFamily}|${fontSize}|${color}|${bold}|${italic}`; if (!this.styles.has(key)) { this.styles.set(key, new TextStyle(fontFamily, fontSize, color, bold, italic)); } return this.styles.get(key)!; } // Monitoring: how effective is our sharing? getStats(): { uniqueStyles: number } { return { uniqueStyles: this.styles.size }; }} // ============================================// ADAPTER: Format-specific parsers// ============================================interface DocumentParser { canParse(file: File): boolean; parse(file: File): Promise<Document>;} class PDFParserAdapter implements DocumentParser { private pdfLib: PDFLibrary; private styleFactory = TextStyleFactory.getInstance(); constructor(pdfLib: PDFLibrary) { this.pdfLib = pdfLib; } canParse(file: File): boolean { return file.mimeType === 'application/pdf' || file.name.endsWith('.pdf'); } async parse(file: File): Promise<Document> { const pdfDoc = await this.pdfLib.load(file.buffer); const pages: Page[] = []; for (let i = 0; i < pdfDoc.pageCount; i++) { const pdfPage = pdfDoc.getPage(i); const elements: PageElement[] = []; for (const textItem of pdfPage.getTextItems()) { // Use flyweight for styles const style = this.styleFactory.getStyle( textItem.fontName, textItem.fontSize, textItem.color, textItem.isBold, textItem.isItalic ); elements.push({ type: 'text', bounds: this.convertBounds(textItem.bounds), style, // Shared flyweight reference content: textItem.text, }); } pages.push({ number: i + 1, text: elements.map(e => e.content).join(' '), elements, }); } return { id: generateId(), pages, metadata: this.extractMetadata(pdfDoc), }; } private convertBounds(pdfBounds: PDFBounds): Rectangle { /* ... */ return {} as Rectangle; } private extractMetadata(doc: PDFDocument): DocumentMetadata { /* ... */ return {} as DocumentMetadata; }} class WordParserAdapter implements DocumentParser { private styleFactory = TextStyleFactory.getInstance(); canParse(file: File): boolean { return file.mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || file.name.endsWith('.docx'); } async parse(file: File): Promise<Document> { const docx = await DocxParser.parse(file.buffer); // Convert Word structure to our Document model // Uses same flyweight factory for style sharing return this.convertToDocument(docx); } private convertToDocument(docx: DocxDocument): Document { /* ... */ return {} as Document; }} class OCRParserAdapter implements DocumentParser { constructor(private ocrService: OCRService) {} canParse(file: File): boolean { return file.mimeType.startsWith('image/') || file.name.match(/\.(png|jpg|jpeg|tiff|bmp)$/i) !== null; } async parse(file: File): Promise<Document> { const ocrResult = await this.ocrService.processImage(file.buffer); // Convert OCR result to our Document model return this.convertToDocument(ocrResult); } private convertToDocument(ocr: OCRResult): Document { /* ... */ return {} as Document; }} // ============================================// BRIDGE: Processing mode separation// ============================================// Abstraction: Document Processorabstract class DocumentProcessor { constructor(protected mode: ProcessingMode) {} abstract process(doc: Document): Promise<ProcessedDocument>; protected async executeWithMode<T>( operation: () => Promise<T>, operationName: string ): Promise<T> { return this.mode.execute(operation, operationName); }} // Implementation: Processing modesinterface ProcessingMode { execute<T>(operation: () => Promise<T>, name: string): Promise<T>;} class BatchProcessingMode implements ProcessingMode { private queue: Array<{ op: () => Promise<unknown>; resolve: Function; reject: Function }> = []; private processing = false; async execute<T>(operation: () => Promise<T>, name: string): Promise<T> { return new Promise((resolve, reject) => { this.queue.push({ op: operation, resolve, reject }); this.processQueue(); }); } private async processQueue(): Promise<void> { if (this.processing || this.queue.length === 0) return; this.processing = true; // Process in batches of 10 for efficiency while (this.queue.length > 0) { const batch = this.queue.splice(0, 10); await Promise.all(batch.map(async item => { try { const result = await item.op(); item.resolve(result); } catch (e) { item.reject(e); } })); } this.processing = false; }} class RealtimeProcessingMode implements ProcessingMode { constructor(private timeout: number = 5000) {} async execute<T>(operation: () => Promise<T>, name: string): Promise<T> { // Execute immediately with timeout for real-time responsiveness return Promise.race([ operation(), new Promise<never>((_, reject) => setTimeout(() => reject(new TimeoutError(`${name} timed out`)), this.timeout) ) ]); }} class StreamingProcessingMode implements ProcessingMode { constructor(private chunkSize: number) {} async execute<T>(operation: () => Promise<T>, name: string): Promise<T> { // For large documents, process in chunks with periodic yielding // to avoid blocking the event loop await new Promise(resolve => setImmediate(resolve)); return operation(); }} // ============================================// DECORATOR: Stackable processing steps// ============================================interface DocumentOperation { apply(doc: Document): Promise<Document>;} // Base processingclass TextExtractor implements DocumentOperation { async apply(doc: Document): Promise<Document> { // Basic text extraction already done by parser return doc; }} // Decorator: Add redaction capabilityclass RedactionDecorator implements DocumentOperation { constructor( private wrapped: DocumentOperation, private patterns: RegExp[] ) {} async apply(doc: Document): Promise<Document> { // First apply wrapped operation const processed = await this.wrapped.apply(doc); // Then add redaction return { ...processed, pages: processed.pages.map(page => ({ ...page, text: this.redact(page.text), elements: page.elements.map(el => ({ ...el, content: this.redact(el.content), })), })), }; } private redact(text: string): string { let result = text; for (const pattern of this.patterns) { result = result.replace(pattern, '[REDACTED]'); } return result; }} // Decorator: Add entity extractionclass EntityExtractionDecorator implements DocumentOperation { constructor( private wrapped: DocumentOperation, private nlpService: NLPService ) {} async apply(doc: Document): Promise<Document> { const processed = await this.wrapped.apply(doc); const fullText = processed.pages.map(p => p.text).join('\n'); const entities = await this.nlpService.extractEntities(fullText); return { ...processed, metadata: { ...processed.metadata, entities, }, }; }} // Decorator: Add classificationclass ClassificationDecorator implements DocumentOperation { constructor( private wrapped: DocumentOperation, private classifier: DocumentClassifier ) {} async apply(doc: Document): Promise<Document> { const processed = await this.wrapped.apply(doc); const classification = await this.classifier.classify(processed); return { ...processed, metadata: { ...processed.metadata, classification, }, }; }} // ============================================// FACADE: Unified document processing interface// ============================================class DocumentProcessingFacade { private parsers: DocumentParser[]; private defaultMode: ProcessingMode; constructor(config: ProcessingConfig) { // Initialize format-specific parsers (Adapters) this.parsers = [ new PDFParserAdapter(config.pdfLib), new WordParserAdapter(), new OCRParserAdapter(config.ocrService), ]; // Default processing mode this.defaultMode = config.defaultBatch ? new BatchProcessingMode() : new RealtimeProcessingMode(); } // Simple interface for common operations async processDocument( file: File, options: ProcessingOptions = {} ): Promise<ProcessedDocument> { // Find appropriate parser (Adapter selection) const parser = this.parsers.find(p => p.canParse(file)); if (!parser) { throw new UnsupportedFormatError(`No parser for ${file.mimeType}`); } // Parse to normalized format const doc = await parser.parse(file); // Build processing pipeline (Decorators) let operation: DocumentOperation = new TextExtractor(); if (options.redact) { operation = new RedactionDecorator(operation, options.redactPatterns || [ /\b\d{3}-\d{2}-\d{4}\b/g, // SSN /\b\d{16}\b/g, // Credit card ]); } if (options.extractEntities) { operation = new EntityExtractionDecorator(operation, this.nlpService); } if (options.classify) { operation = new ClassificationDecorator(operation, this.classifier); } // Apply processing with appropriate mode (Bridge) const mode = options.realtime ? new RealtimeProcessingMode(options.timeout) : this.defaultMode; return mode.execute(() => operation.apply(doc), 'DocumentProcessing'); } // Batch processing for efficiency async processBatch( files: File[], options: ProcessingOptions = {} ): Promise<ProcessedDocument[]> { const batchMode = new BatchProcessingMode(); return Promise.all(files.map(file => this.processDocument(file, { ...options, realtime: false }) )); }} // ============================================// Usage Example// ============================================const processor = new DocumentProcessingFacade(config); // Simple usage - facade hides all complexityconst result = await processor.processDocument(uploadedFile, { redact: true, extractEntities: true, classify: true,}); // Batch processing - same simple interfaceconst results = await processor.processBatch(uploadedFiles, { redact: true,});Context:
A company needs a UI component library that works across Web, iOS, and Android platforms with shared business logic but platform-specific rendering. Requirements:
Forces Analysis:
| Force | Pattern Implication |
|---|---|
| Component × Platform variation | Bridge separates component from renderer |
| Arbitrary nesting (parent contains children) | Composite for tree structure |
| Lazy loading heavy components | Proxy for deferred creation |
| Shared theming | Flyweight for shared style objects |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
// ============================================// BRIDGE: Component abstraction × Platform rendering// ============================================// Implementation: Platform-specific renderersinterface PlatformRenderer { renderText(props: TextProps): NativeElement; renderButton(props: ButtonProps): NativeElement; renderContainer(props: ContainerProps, children: NativeElement[]): NativeElement; renderImage(props: ImageProps): NativeElement;} class WebRenderer implements PlatformRenderer { renderText(props: TextProps): HTMLElement { const span = document.createElement('span'); span.textContent = props.content; span.style.cssText = this.propsToCSS(props.style); return span; } renderButton(props: ButtonProps): HTMLElement { const button = document.createElement('button'); button.textContent = props.label; button.onclick = props.onPress; button.className = this.buttonVariantClass(props.variant); return button; } renderContainer(props: ContainerProps, children: NativeElement[]): HTMLElement { const div = document.createElement('div'); div.style.cssText = this.layoutToCSS(props.layout); children.forEach(child => div.appendChild(child as HTMLElement)); return div; } renderImage(props: ImageProps): HTMLElement { const img = document.createElement('img'); img.src = props.source; img.alt = props.alt; return img; } private propsToCSS(style: TextStyle): string { /* ... */ return ''; } private layoutToCSS(layout: LayoutProps): string { /* ... */ return ''; } private buttonVariantClass(variant: string): string { /* ... */ return ''; }} class IOSRenderer implements PlatformRenderer { renderText(props: TextProps): UILabel { const label = new UILabel(); label.text = props.content; label.font = this.propsToFont(props.style); label.textColor = this.colorFromHex(props.style.color); return label; } renderButton(props: ButtonProps): UIButton { const button = new UIButton(UIButtonType.system); button.setTitle(props.label, UIControlState.normal); button.addTarget(props.onPress, UIControlEvent.touchUpInside); return button; } renderContainer(props: ContainerProps, children: NativeElement[]): UIView { const view = new UIStackView(); view.axis = props.layout.direction === 'row' ? UILayoutConstraintAxis.horizontal : UILayoutConstraintAxis.vertical; children.forEach(child => view.addArrangedSubview(child as UIView)); return view; } renderImage(props: ImageProps): UIImageView { const imageView = new UIImageView(); imageView.load(props.source); return imageView; } private propsToFont(style: TextStyle): UIFont { /* ... */ return {} as UIFont; } private colorFromHex(hex: string): UIColor { /* ... */ return {} as UIColor; }} class AndroidRenderer implements PlatformRenderer { renderText(props: TextProps): TextView { const textView = new TextView(this.context); textView.setText(props.content); textView.setTextSize(props.style.fontSize); textView.setTextColor(Color.parseColor(props.style.color)); return textView; } // ... similar implementations for other methods} // ============================================// COMPOSITE: Component tree structure// ============================================abstract class UIComponent { protected children: UIComponent[] = []; protected renderer: PlatformRenderer; protected theme: Theme; constructor(renderer: PlatformRenderer, theme: Theme) { this.renderer = renderer; this.theme = theme; } abstract render(): NativeElement; addChild(child: UIComponent): void { this.children.push(child); } removeChild(child: UIComponent): void { const index = this.children.indexOf(child); if (index > -1) { this.children.splice(index, 1); } }} // Leaf componentclass TextComponent extends UIComponent { constructor( private props: TextProps, renderer: PlatformRenderer, theme: Theme ) { super(renderer, theme); } render(): NativeElement { return this.renderer.renderText({ ...this.props, style: this.theme.resolveTextStyle(this.props.style), }); }} // Leaf componentclass ButtonComponent extends UIComponent { constructor( private props: ButtonProps, renderer: PlatformRenderer, theme: Theme ) { super(renderer, theme); } render(): NativeElement { return this.renderer.renderButton({ ...this.props, variant: this.theme.resolveButtonVariant(this.props.variant), }); }} // Composite component (can contain children)class ContainerComponent extends UIComponent { constructor( private props: ContainerProps, renderer: PlatformRenderer, theme: Theme ) { super(renderer, theme); } render(): NativeElement { const renderedChildren = this.children.map(child => child.render()); return this.renderer.renderContainer( { ...this.props, layout: this.theme.resolveLayout(this.props.layout), }, renderedChildren ); }} // ============================================// PROXY: Lazy loading for heavy components// ============================================class LazyImageComponent extends UIComponent { private loaded = false; private actualComponent: UIComponent | null = null; constructor( private imageProps: ImageProps, renderer: PlatformRenderer, theme: Theme ) { super(renderer, theme); } render(): NativeElement { if (!this.loaded) { // Return placeholder while loading return this.renderer.renderText({ content: '⏳', style: this.theme.getPlaceholderStyle() }); } return this.renderer.renderImage(this.imageProps); } // Called when component becomes visible onVisible(): void { if (!this.loaded) { // Preload image ImageLoader.preload(this.imageProps.source).then(() => { this.loaded = true; this.requestRerender(); }); } } private requestRerender(): void { // Trigger re-render through framework }} // ============================================// FLYWEIGHT: Shared theme objects// ============================================class Theme { private textStyles = new Map<string, ResolvedTextStyle>(); private buttonStyles = new Map<string, ResolvedButtonStyle>(); private layoutStyles = new Map<string, ResolvedLayout>(); constructor(private themeDefinition: ThemeDefinition) {} resolveTextStyle(props: TextStyleProps): ResolvedTextStyle { const key = JSON.stringify(props); if (!this.textStyles.has(key)) { this.textStyles.set(key, this.computeTextStyle(props)); } return this.textStyles.get(key)!; } resolveButtonVariant(variant: string): ResolvedButtonStyle { if (!this.buttonStyles.has(variant)) { this.buttonStyles.set(variant, this.computeButtonStyle(variant)); } return this.buttonStyles.get(variant)!; } resolveLayout(props: LayoutProps): ResolvedLayout { const key = JSON.stringify(props); if (!this.layoutStyles.has(key)) { this.layoutStyles.set(key, this.computeLayout(props)); } return this.layoutStyles.get(key)!; } private computeTextStyle(props: TextStyleProps): ResolvedTextStyle { return { fontFamily: props.fontFamily || this.themeDefinition.typography.defaultFont, fontSize: props.fontSize || this.themeDefinition.typography.defaultSize, color: props.color || this.themeDefinition.colors.text, fontWeight: props.bold ? 'bold' : 'normal', }; } private computeButtonStyle(variant: string): ResolvedButtonStyle { const variantDef = this.themeDefinition.buttons[variant]; return { backgroundColor: variantDef.background, textColor: variantDef.text, borderRadius: variantDef.borderRadius, padding: variantDef.padding, }; } private computeLayout(props: LayoutProps): ResolvedLayout { return { direction: props.direction || 'column', gap: props.gap || this.themeDefinition.spacing.default, padding: props.padding || 0, }; }} // ============================================// Factory: Create components with platform awareness// ============================================class UIComponentFactory { constructor( private renderer: PlatformRenderer, private theme: Theme ) {} text(props: TextProps): TextComponent { return new TextComponent(props, this.renderer, this.theme); } button(props: ButtonProps): ButtonComponent { return new ButtonComponent(props, this.renderer, this.theme); } container(props: ContainerProps): ContainerComponent { return new ContainerComponent(props, this.renderer, this.theme); } lazyImage(props: ImageProps): LazyImageComponent { return new LazyImageComponent(props, this.renderer, this.theme); }} // ============================================// Usage Example// ============================================// Platform detectionconst renderer = Platform.isWeb() ? new WebRenderer() : Platform.isIOS() ? new IOSRenderer() : new AndroidRenderer(); const theme = new Theme(modernLightTheme);const ui = new UIComponentFactory(renderer, theme); // Build component treeconst card = ui.container({ layout: { direction: 'column', gap: 16 } });card.addChild(ui.lazyImage({ source: '/avatar.jpg', alt: 'User' }));card.addChild(ui.text({ content: 'John Doe', style: { fontSize: 18, bold: true } }));card.addChild(ui.text({ content: 'Software Engineer', style: { color: '#666' } }));card.addChild(ui.button({ label: 'Follow', variant: 'primary', onPress: handleFollow })); // Render to native platformconst nativeElement = card.render();Analyzing these real-world case studies reveals consistent patterns in how experienced engineers approach pattern selection.
In production systems, you'll use Adapter, Decorator, Proxy, and Facade far more often than Composite, Bridge, or Flyweight. The first four address common, recurring problems. The latter three are for specialized scenarios. Master the common patterns first; recognize when the specialized patterns are needed.
Even with understanding, certain mistakes recur in production codebases. Learning from others' errors accelerates your growth.
| Mistake | Symptom | Solution |
|---|---|---|
| Decorator without interface | Decorators have different methods than decorated; can't be composed | Ensure decorator implements same interface as wrapped object |
| Proxy that changes behavior | Proxy adds features instead of controlling access | Use Decorator for behavior addition; Proxy is for access control |
| Facade that grows forever | Facade has 50+ methods; becomes god class | Split into multiple focused facades or introduce intermediate abstractions |
| Adapter per use case instead of per adaptee | Similar adapters proliferate; duplication | One adapter per external system, parameterized for variations |
| Flyweight with mutable state | Shared objects modified, causing cross-contamination | Flyweights must be immutable; freeze object or use immutable pattern |
| Composite with type checking | Code checks 'is this a leaf or composite?' everywhere | Push behavior variation into polymorphic methods; avoid type checks |
| Bridge without variation | Bridge abstraction with single implementation | Bridge is for avoiding class explosion; if only one implementation, use simpler composition |
Through this module, you've developed a comprehensive framework for structural pattern selection and application. Let's consolidate your mastery:
The Path Forward:
Structural patterns are one of three pattern categories. Having mastered structural patterns, you're prepared to explore:
Each category addresses different design challenges, and the skills you've developed—systematic comparison, decision trees, combination analysis—transfer to those domains as well.
Congratulations! You've completed the Structural Patterns chapter, including the capstone module on pattern selection. You now have Principal Engineer-level understanding of how to compose objects and classes into larger structures. Apply this knowledge deliberately, review production code through a pattern lens, and the patterns will become second nature.