Loading learning content...
We've established that we cannot predict all future changes and that over-engineering carries real costs. Yet some extension points clearly provide value—interfaces that have spawned multiple implementations, abstraction boundaries that have contained change, plugin systems that have enabled ecosystems.
The question isn't whether to design for extensibility—it's where. Strategic extension points are places in your architecture where flexibility investments consistently pay off, where the probability of valuable variation is high enough to justify the abstraction cost.
This page provides frameworks for identifying these high-leverage locations and distinguishing them from areas where simplicity should reign.
By the end of this page, you will understand the characteristics of high-value extension points, learn decision frameworks for where to invest in flexibility, and develop intuition for architectural seams that naturally accommodate variation. You'll move from uniform abstraction to targeted, strategic extensibility.
Strategic extension points share common characteristics. Understanding these helps identify where abstraction investment is likely to succeed.
Properties of valuable extension points:
| Characteristic | Why It Matters | Signal Strength |
|---|---|---|
| Observable Variation Pattern | You've already seen multiple variations emerge or change | Strong — evidence over speculation |
| External Boundary | Integration with external systems that you don't control | Strong — external change is inevitable |
| Policy/Implementation Split | Business rules distinct from technical implementation | Strong — business rules change frequently |
| Industry-Standard Variation | The domain inherently has known categories (e.g., payment types) | Medium — only if variation is real, not theoretical |
| Stakeholder-Identified Variability | Business stakeholders explicitly describe likely changes | Medium — depends on stakeholder accuracy |
| Historical Pattern | Similar systems consistently needed this extension | Medium — context must match |
| Regulatory Sensitivity | Areas subject to compliance requirements | High — regulatory change is certain |
The evidence hierarchy:
Notice that the strongest signals come from observation, not prediction. Extension points where you've already seen variation are the safest investments. Speculative extension points—those based on imagination rather than experience—carry much higher risk.
Ranking by evidence:
A practical approach: The first time you modify code for a new variation, do it directly. The second time, note the pattern. The third time, refactor to an extension point. By the third occurrence, you have evidence of real variation—and enough examples to design an abstraction that actually fits.
The boundaries between your system and external systems are natural extension points. You don't control external systems, so they will change on their schedule, not yours.
Types of external boundaries:
12345678910111213141516171819202122232425
// External boundaries merit abstraction// because external change is outside your control // Payment processing - known high-variation areainterface PaymentGateway { charge(amount: Money, paymentMethod: PaymentMethod): Promise<ChargeResult>; refund(chargeId: string, amount?: Money): Promise<RefundResult>; getCharge(chargeId: string): Promise<Charge>;} // Implementations for different providersclass StripeGateway implements PaymentGateway { /* ... */ }class BraintreeGateway implements PaymentGateway { /* ... */ }class AdyenGateway implements PaymentGateway { /* ... */ } // The abstraction cost is justified because:// 1. Payment providers ARE different - real variation exists// 2. Business needs drive provider changes (fees, features, markets)// 3. Testing against real providers is expensive - mocking helps// 4. Compliance may require provider changes // Compare to internal code where you control both sides// - No external change pressure// - Single implementation is fine// - Abstraction cost isn't justified by evidenceAnti-corruption layers:
External systems often have models that don't fit your domain. An abstraction at the boundary serves two purposes: enabling extension AND translating between external models and your domain. This dual value further justifies the abstraction cost.
Example: A payment provider's Transaction object might include 50 fields for their analytics. Your domain only cares about amount, status, and ID. The abstraction boundary translates, keeping external complexity from polluting your domain.
External boundary abstractions provide testing value even if you never swap implementations. Mocking external services for unit tests, simulating error conditions, and testing without network dependencies all become possible. This testing value alone often justifies the abstraction cost.
One of the most reliable sources of valuable extension points is the separation between policy (what the system should do) and implementation (how it does it). Business policies change frequently; technical implementations change less often.
Distinguishing policy from implementation:
Why this separation creates good extension points:
Policies are business decisions. Businesses pivot, experiment, respond to competition, and optimize continuously. Policy changes are expected and frequent. By isolating policy from implementation:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
// Policy as extension point - strategies for business rules interface ShippingPolicy { calculateShipping(order: Order): Money;} // Policies can change frequently - new rules, thresholds, experimentsclass StandardShippingPolicy implements ShippingPolicy { calculateShipping(order: Order): Money { // Standard rate calculation return this.rateCalculator.calculate(order.weight, order.destination); }} class FreeShippingThresholdPolicy implements ShippingPolicy { constructor( private threshold: Money, private fallback: ShippingPolicy ) {} calculateShipping(order: Order): Money { if (order.subtotal.gte(this.threshold)) { return Money.zero(); } return this.fallback.calculateShipping(order); }} class MembershipShippingPolicy implements ShippingPolicy { constructor( private membershipService: MembershipService, private memberRate: Percentage, private fallback: ShippingPolicy ) {} calculateShipping(order: Order): Money { const baseShipping = this.fallback.calculateShipping(order); if (this.membershipService.isPremium(order.customerId)) { return baseShipping.multiply(this.memberRate); } return baseShipping; }} // The order service doesn't know about policy details// It just uses the injected policy - open for new policiesclass OrderService { constructor(private shippingPolicy: ShippingPolicy) {} calculateTotal(order: Order): Money { const subtotal = order.subtotal; const shipping = this.shippingPolicy.calculateShipping(order); const tax = this.taxService.calculate(order); return subtotal.add(shipping).add(tax); }} // New business rules = new ShippingPolicy implementations// OrderService doesn't change - closed for modificationNot every business rule deserves an extension point. If a policy has been stable for years and shows no signs of variation, a simple implementation is fine. Abstracting stable policies adds complexity without benefit. Focus abstraction on policies that actually vary or that stakeholders explicitly identify as likely to change.
A seam is a place in your architecture where you can insert variation without disturbing surrounding code. Strategic seams are locations that naturally accommodate change. Here's a framework for identifying them:
Evaluating seam quality:
Not all seams are equally valuable. Evaluate potential extension points against these criteria:
| Criterion | High Value | Low Value |
|---|---|---|
| Change Frequency | Frequent historical changes or expected changes | Stable for years; no expected changes |
| Change Independence | Changes to one side don't require changes to the other | Coupled changes; both sides must change together |
| Variation Count | Multiple implementations exist or are expected | Only one implementation ever expected |
| Stakeholder Interest | Business stakeholders care about variability here | Technical implementation detail only |
| Testing Value | Seam enables valuable testing isolation | Seam doesn't improve testing |
| Cost/Benefit Ratio | Abstraction cost is low relative to flexibility value | High abstraction cost; unclear benefit |
When designing a new system, don't force extension points. Instead, structure your code so it has clean boundaries (layers, modules, services). These natural boundaries become seams that you can formalize into extension points if and when variation actually appears. Clean structure now enables extensibility later.
Here's a practical decision framework for evaluating whether a location deserves investment in an extension point:
12345678910111213141516171819202122232425262728293031323334
## Extension Point Decision Framework ### Question 1: Is there OBSERVABLE variation?- Already have multiple implementations? → EXTEND (formalize the abstraction)- Already modified this code 3+ times for variation? → EXTEND- Only theoretical variation? → Proceed to Question 2 ### Question 2: Is this an EXTERNAL boundary?- Third-party systems you don't control? → EXTEND (justified by external change)- Internal systems you fully control? → Proceed to Question 3 ### Question 3: What is the COST of extension?- Simple interface with clear contract? → Low cost, proceed to Question 4- Complex abstraction with many methods? → High cost, need strong justification- Requires framework/infrastructure? → Very high cost, very strong justification needed ### Question 4: What is the BENEFIT of extension?- Enables critical business capability? → Strong benefit- Enables valuable testing? → Moderate benefit - Just seems like "good design"? → Weak benefit, probably not worth the cost ### Decision Matrix:| Variation | Boundary | Cost | Benefit | Decision ||-----------|----------|------|---------|----------|| Observable | External | Any | Any | Extend || Observable | Internal | Low | Any | Extend || Observable | Internal | High | Strong | Extend || Observable | Internal | High | Weak | Wait || Theoretical | External | Low | Moderate+ | Extend || Theoretical | External | High | Strong | Consider || Theoretical | Internal | Any | Any | Wait | ### Default: When in doubt, WAITBetter to add extension later with knowledge than now with speculation.Applying the framework:
Example 1: Payment Processing
Example 2: Internal Pricing Calculation
Example 3: Notification Channels
Example 4: Report Generation
When you decide NOT to add an extension point, document why. 'We chose not to abstract report generation because only HTML is needed now. If PDF or Excel are required, we'll introduce an abstraction then.' This context helps future developers understand the decision and know when to reconsider.
Based on decades of software engineering experience, certain extension points appear so frequently across systems that they merit special consideration. This catalog provides guidance but not mandates—always verify with evidence from your specific context.
| Extension Point | Why It Works | Caveat |
|---|---|---|
| Authentication/Authorization | Security requirements evolve; providers change | Don't over-abstract if using established framework |
| Payment Processing | Business and compliance requirements drive provider changes | Most payment libs provide own abstraction |
| Notification Dispatch | Channels multiply (email→SMS→push→in-app→...) | Wait until second channel is needed |
| External Data Import | Source formats and systems change | Validate that multiple sources are expected |
| Content Rendering/Templating | Presentation needs evolve rapidly | Framework usually provides; don't duplicate |
| Audit/Logging Infrastructure | Compliance and operational needs evolve | But logging framework handles most cases |
| Feature Flags/Configuration | Business needs to control features dynamically | Use established feature flag service if possible |
| Pricing/Discount Rules | Business experiments continuously | But simple rules may not need abstraction |
| Search/Filtering | Query needs grow in complexity | Especially if search engine might change |
| File/Media Storage | Storage providers change; requirements scale | Cloud storage services change often |
Using the catalog wisely:
This catalog is not an instruction to abstract everything listed. Instead, use it as a checklist for evaluation:
Areas that DON'T typically need extension points:
A startup building an MVP needs almost no extension points—speed matters more than flexibility. An enterprise building a 10-year platform may need more. A product with one customer has different needs than a multi-tenant SaaS. Always consider your specific context over general guidance.
When you decide an extension point is warranted, prefer low-cost techniques. These provide flexibility with minimal abstraction overhead.
interface Sender { send(message: Message): Promise<void> } costs almost nothing.processOrder(order, calculateShipping) allows variation without interface proliferation.strategies[type](args) is often sufficient.1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
// LOW-COST: Simple function parameterfunction processOrder( order: Order, calculateShipping: (order: Order) => Money = defaultShipping): Receipt { const shipping = calculateShipping(order); // ... rest of processing} // LOW-COST: Map-based strategy selectionconst discountStrategies: Record<string, DiscountStrategy> = { 'percentage': percentageDiscount, 'fixed': fixedDiscount, 'tiered': tieredDiscount,}; function applyDiscount(order: Order, type: string): Order { const strategy = discountStrategies[type] ?? noDiscount; return strategy(order);} // LOW-COST: Event-based extensionclass OrderProcessor { private eventEmitter = new EventEmitter(); onOrderProcessed(callback: (order: Order) => void) { this.eventEmitter.on('processed', callback); } process(order: Order) { // ... processing this.eventEmitter.emit('processed', order); }} // Listeners can be added without modifying OrderProcessor// Extension happens through subscription, not interface implementation // HIGH-COST (avoid unless necessary): Elaborate abstractioninterface OrderProcessorFactory { create(context: ProcessorContext): OrderProcessor;}interface ProcessorContext { /* many fields */ }class ConcreteOrderProcessorFactory implements OrderProcessorFactory { // ... lots of complexity}// Only justified if the complexity is truly neededLow-cost techniques often provide 80% of the flexibility for 20% of the complexity cost. If a simple function parameter gives you the extension capability you need, don't build a factory-strategy-decorator stack. Match the technique to the actual variation, not the imagined maximum variation.
We've explored how to identify where extensibility investments are most likely to pay off, moving from uniform abstraction to strategic, evidence-based extensibility.
What's next:
We've now covered why prediction fails, the costs of over-engineering, and how to identify strategic extension points. The final page brings these concepts together into an iterative approach—applying OCP incrementally as understanding develops, rather than trying to get it right upfront.
You now have frameworks for identifying high-value extension points and distinguishing them from areas where simplicity should prevail. The key insight: focus extensibility investments where evidence supports them—external boundaries, observed variation, and policy-implementation splits. Next, we'll learn to apply OCP iteratively as our understanding evolves.