future-proofing
rule
future-proof code preserves the right facts, boundaries, and evidence for an approved direction. it does not predict imaginary requirements with generic frameworks, fallback paths, aliases, or configuration flags.
why
speculative abstraction is debt taken on for a requirement nobody approved: it multiplies code paths, hides which contract is live, and still gets rewritten when the real requirement arrives because its shape was guessed. a small explicit solution behind a real boundary is easier to replace precisely because it is honest about what exists today. invariant: every extension point answers to an approved decision with a known boundary and acceptance criteria — never to imagination.
design for the next approved change
- one shortest exact name and one canonical symbol per domain concept.
- explicit boundaries for UI, domain rules, persistence, provider adapters, runtime entrypoints.
- contracts at boundaries: schemas for input, typed outcomes for failures, generated clients for APIs, migrations for persisted change.
- preserve canonical identifiers and durable facts; derive views from them.
- side effects idempotent and observable: correlation identity, operation name, actionable failure outcome.
- keep the owning slice current so a later agent traces expectation to code, data, and verification without archaeology.
three horizons
| horizon | decision | code response |
|---|---|---|
| current | required behavior exists now. | implement it directly and verify it. |
| approved next | a concrete decision has a known boundary and acceptance criteria. | leave a focused extension point at that boundary, with its contract documented. |
| speculation | a possible future idea has no approved outcome. | do not abstract, configure, alias, or preserve compatibility for it. |
durable extension points
an extension point is justified only with two real, independently changing implementations, or a contract that requires substitution — e.g. a provider adapter behind a stable port, a generated API client, a migration-owned database boundary. name its responsibility exactly; keep the dependency direction one-way.
remove an obsolete path once the approved target changes. compatibility code, dual writes, aliases, and permissive fallbacks make maintenance less safe because they conceal which contract is live.
future-proof means easier replacement, not more abstraction
a small, explicit current solution behind a real boundary is easier to change than a generic framework built for requirements nobody approved.
review questions
- which known future change does this boundary serve?
- what exact contract protects the current behavior?
- can a later agent locate the owner from its domain word, slice, and symbol?
- does persistence preserve canonical facts and migration history?
- would removing a fallback reveal an unhandled product state? if yes, model that state explicitly rather than retaining an invisible fallback.
do not
- add options "for flexibility" without an approved consumer.
- create a generic base class, helper, hook, or package for one use.
- preserve old names or behavior solely in case someone still calls it.
- move an unresolved product decision into configuration.
- claim a future direction is safe without a current slice and verification evidence.
good and bad
bad
one payment provider (Stripe), plus a speculative generic framework for a future Adyen integration that may never be approved.
// src/payment/providerFactory.ts — speculative abstractioninterface PaymentProvider {charge(amount: number): Promise<ChargeResult>;refund(chargeId: string): Promise<RefundResult>;isAvailable(): Promise<boolean>;}export function createProvider(name: string): PaymentProvider {switch (name) {case "stripe":return new StripeAdapter();case "adyen":return new AdyenAdapter(); // not approved, not implementedcase "fallback":return new MockAdapter(); // phantom optiondefault:throw new Error("Unknown provider");}}// src/payment/stripeAdapter.tsclass StripeAdapter implements PaymentProvider {async charge(amount: number) {/* ... */}async refund(chargeId: string) {/* ... */}async isAvailable() {/* ... */}}// src/payment/adyenAdapter.ts — empty, speculativeclass AdyenAdapter implements PaymentProvider {async charge() {throw new Error("not implemented");}async refund() {throw new Error("not implemented");}async isAvailable() {return false;}}
consequences:
- three implementations exist (Stripe, Adyen, Mock); only Stripe is real. the interface hides that reality.
- a bug in
PaymentProvider.chargeaffects three code paths equally — blame assignment and rollback get harder. - when Adyen is actually approved, the interface may not match Adyen's real capabilities, forcing a rewrite anyway.
- every payment call routes through the factory, adding navigation cost for no benefit today.
good
Stripe integrated directly through one named, specific adapter. no speculative framework.
// src/payment/stripeAdapter.tsexport async function chargeWithStripe(params: ChargeParams): Promise<ChargeResult> {const response = await stripe.charges.create({amount: params.amountCents,currency: "usd",customer: params.customerId,idempotency_key: params.idempotencyKey,});return {id: response.id,amountCents: response.amount,status: response.status as ChargeStatus,};}export async function refundWithStripe(chargeId: string): Promise<RefundResult> {const response = await stripe.refunds.create({ charge: chargeId });return {id: response.id,amountCents: response.amount,status: response.status as RefundStatus,};}
// src/payment/route.ts — clear naming, no abstraction theaterexport async function handleChargeRequest(req: ChargeRequest) {const result = await chargeWithStripe(req);return result;}
when Adyen is actually approved and funded:
- read the real Adyen API contract.
- create
adyenAdapter.tswith an exactly matching implementation. - introduce a real
PaymentProviderPortinterface only after two concrete implementations exist and differ in meaningful ways. - update one call site at a time to route through the port.
the speculative code from today is gone, replaced by a real decision rooted in concrete evidence. the current implementation is clear, easy to debug, and ready to be replaced when the real requirement arrives.