Loading learning content...
No matter how thoroughly you gather requirements, no matter how clearly stakeholders articulate their needs, one truth is absolute: requirements will change. Markets shift, user feedback arrives, competitive pressures emerge, and regulations evolve. A design that cannot accommodate change is not a finished product—it's a ticking time bomb.
Extensibility validation ensures that your design is not merely correct today, but prepared for the changes that tomorrow will inevitably bring. This page establishes a rigorous framework for assessing and improving your design's adaptability before implementation begins.
By the end of this page, you will understand how to identify anticipated change scenarios, evaluate design flexibility through concrete analysis techniques, recognize extension point patterns, and validate that your design's architecture supports evolution without architectural surgery.
Extensibility is the capacity of a software design to accommodate new requirements, features, or behaviors with minimal modification to existing code. It is not the same as flexibility, generality, or over-engineering. Extensibility is strategic—it targets specific, anticipated directions of change.
The Three Dimensions of Extensibility:
The Cost-Benefit Balance:
Extensibility is not free. Every extension point adds complexity:
The goal is not maximum extensibility, but strategic extensibility—flexibility where change is anticipated, simplicity where it is not. Validating extensibility means confirming that extension points exist where needed and complexity is not added where it isn't.
YAGNI (You Aren't Gonna Need It) cautions against building for imaginary requirements. But YAGNI applies to specific features, not architectural flexibility. A design that cannot evolve violates a different principle: invest in the obvious, prepare for the probable, ignore the improbable.
Effective extensibility validation begins with explicitly identifying what changes are likely. This is not speculation—it's informed analysis based on domain knowledge, stakeholder input, and industry patterns.
Sources of Change Intelligence:
Documenting Change Scenarios:
For each identified change scenario, document:
| Scenario | Probability | Timeline | Affected Areas | Design Response |
|---|---|---|---|---|
| Add cryptocurrency payment | High | 6-12 months | Payment processing, receipts | Payment strategy interface, polymorphic processors |
| Support multi-language content | Medium | 12-18 months | UI, notifications, reports | Content abstraction with locale awareness |
| Real-time inventory sync | High | 3-6 months | Inventory, orders, caching | Event-driven inventory with observer pattern |
| White-label branding | Low | 18+ months | UI, email templates, PDF generation | Theme/brand configuration injection |
| GDPR-style data export | Medium | 6-12 months | All data stores, user management | Data export interface per entity type |
Change Impact Analysis is a systematic technique for evaluating how well a design accommodates anticipated changes. For each change scenario, trace through the design and count the modifications required.
The Impact Scoring Framework:
| Score | Description | Example | Design Quality Indicator |
|---|---|---|---|
| 0 - Configuration Only | Change requires only configuration, no code changes | Toggle a feature flag, adjust a setting | Excellent - design anticipated this change perfectly |
| 1 - Single Addition | Change requires adding one new class/component | Add new PaymentStrategy implementation | Very Good - proper abstraction exists |
| 2-3 - Localized Changes | Change affects 2-3 related components | Add new entity with repository and service | Good - changes are cohesive and contained |
| 4-6 - Cross-Cutting Impact | Change affects multiple unrelated components | Adding auditing across all services | Fair - missing abstraction for this concern |
| 7+ - Architectural Surgery | Change requires restructuring core components | Changing from sync to async processing model | Poor - fundamental architecture doesn't support change |
Performing Change Impact Analysis:
Select a Change Scenario: Choose a high-probability anticipated change from your documented scenarios.
Trace the Change Path: Walk through the design, identifying every class, interface, and component that would need modification or addition.
Categorize Modifications:
Calculate the Impact Score: Count the number of existing files that require modification (new files don't count against extensibility).
Evaluate Against Threshold: High-probability changes should score 0-2. Medium-probability changes should score 0-4. Any change scoring 7+ indicates an extensibility problem.
12345678910111213141516171819202122232425
// Change Scenario: Add new "BankTransfer" payment method // ✅ Well-designed system: Impact Score = 1 (Single Addition)// Files to CREATE (don't count against score):// - BankTransferPaymentStrategy.ts (new)// - BankTransferPaymentStrategyTest.ts (new) // Files to MODIFY:// - PaymentStrategyRegistry.ts (add registration) ← 1 modification // The design anticipated this type of extension // ❌ Poorly-designed system: Impact Score = 8 (Architectural Surgery)// Files to MODIFY:// - OrderService.ts (add bank transfer handling)// - PaymentController.ts (add bank transfer endpoint)// - PaymentRepository.ts (add bank transfer tables)// - ReceiptGenerator.ts (add bank transfer receipt format)// - RefundService.ts (add bank transfer refund logic)// - PaymentValidator.ts (add bank transfer validation)// - ReportGenerator.ts (add bank transfer reporting)// - EmailService.ts (add bank transfer confirmation template) // The design has no abstraction for payment types - // each type is hardcoded throughout the systemIf a single logical change requires modifications scattered across many classes, you've identified 'shotgun surgery'—a design smell indicating missing abstractions. The cure is to extract the commonality into an abstraction that localizes future changes.
Extension points are well-defined locations in a design where new behavior can be added without modifying existing code. During extensibility validation, verify that appropriate extension point patterns are used where anticipated change exists.
Common Extension Point Patterns:
1234567891011121314151617181920212223242526
// Strategy-Based Extension Point interface PricingStrategy { calculatePrice(order: Order): Money; applicableTo(customer: Customer): boolean;} class PricingEngine { constructor(private strategies: PricingStrategy[]) {} getPrice(order: Order, customer: Customer): Money { const strategy = this.strategies.find(s => s.applicableTo(customer)); return strategy?.calculatePrice(order) ?? this.defaultPrice(order); }} // Extension: Add new pricing strategy without modifying PricingEngineclass EnterpriseVolumeDiscount implements PricingStrategy { applicableTo(customer: Customer): boolean { return customer.tier === "enterprise" && customer.annualVolume > 100000; } calculatePrice(order: Order): Money { return order.subtotal.multiply(0.75); // 25% discount }}1234567891011121314151617181920212223242526272829303132333435363738394041424344
// Event-Based Extension Point interface DomainEvent { readonly occurredAt: Date; readonly aggregateId: string;} class OrderPlaced implements DomainEvent { constructor( readonly occurredAt: Date, readonly aggregateId: string, readonly order: Order ) {}} class OrderService { constructor(private eventPublisher: EventPublisher) {} placeOrder(order: Order): void { // Core logic this.validateOrder(order); this.reserveInventory(order); // Publish event for extensible reactions this.eventPublisher.publish( new OrderPlaced(new Date(), order.id, order) ); }} // Extension: Add new reactions without modifying OrderServiceclass LoyaltyPointsHandler { @SubscribeTo(OrderPlaced) handle(event: OrderPlaced): void { this.loyaltyService.awardPoints(event.order.customerId, event.order.total); }} class FraudDetectionHandler { @SubscribeTo(OrderPlaced) handle(event: OrderPlaced): void { this.fraudDetector.checkOrder(event.order); }}Apply this comprehensive checklist during design validation to ensure extensibility is properly addressed:
During extensibility validation, watch for these common failure patterns that undermine design flexibility:
| Anti-Pattern | Symptoms | Impact | Remedy |
|---|---|---|---|
| Hardcoded Type Discrimination | switch/if-else on type field, instanceof checks scattered through code | Every new type requires modifying every switch statement | Introduce polymorphism with strategy or visitor pattern |
| God Class | One class doing everything, 1000+ lines, dozens of methods | Any change risks breaking unrelated functionality | Extract cohesive responsibilities into focused classes |
| Premature Concretization | Using concrete classes where interfaces could allow variation | Cannot substitute alternative implementations | Program to interfaces, inject dependencies |
| Missing Seams | No clear boundaries between components, everything directly coupled | Testing is impossible without infrastructure | Introduce interfaces at component boundaries |
| Configuration Scatter | Configuration values embedded throughout code | Changing a setting requires hunting through codebase | Centralize configuration with injection |
| Implicit Dependencies | Components reach out to obtain dependencies (Service Locator, statics) | Cannot easily substitute dependencies for testing or extension | Explicit constructor injection |
❌ Hardcoded Type Discrimination:
class NotificationService { send(notification: Notification): void { switch (notification.type) { case "email": this.sendEmail(notification); break; case "sms": this.sendSMS(notification); break; case "push": this.sendPush(notification); break; // Adding Slack requires editing here! default: throw new Error("Unknown type"); } } private sendEmail(n: Notification) { } private sendSMS(n: Notification) { } private sendPush(n: Notification) { }}✅ Polymorphic Solution:
interface NotificationChannel { supports(notification: Notification): boolean; send(notification: Notification): void;} class NotificationService { constructor( private channels: NotificationChannel[] ) {} send(notification: Notification): void { const channel = this.channels.find( c => c.supports(notification) ); if (!channel) { throw new Error("No channel for notification"); } channel.send(notification); }} // Extension: Add Slack without modifying serviceclass SlackChannel implements NotificationChannel { supports(n: Notification): boolean { return n.type === "slack"; } send(n: Notification): void { /* ... */ }}Extensibility validation must include a sanity check against over-engineering. Not every part of the design needs extension points. The goal is strategic extensibility, not universal flexibility.
The Probability-Driven Approach:
Apply different levels of extensibility investment based on change probability:
Signs of Over-Engineering:
The Rule of Three:
A useful heuristic: wait until you have three variations before creating an abstraction. The first two cases often reveal that the variation isn't what you thought. By the third, you understand the pattern well enough to create a good abstraction.
However, apply judgment: if the cost of abstraction is low and the probability of variation is high, don't wait artificially.
You now have a comprehensive framework for extensibility validation, including change scenario identification, impact analysis, extension point patterns, and balance against over-engineering. The next page focuses on simplicity checking—ensuring your design achieves its goals without unnecessary complexity.