Evolving in Two Dimensions: Interface Strategies for Real‑World Software(Expression Problem, Part 2)

Patterns that survive both behaviour and variant volatility
Part 1 showed how closed vs. open interfaces trade off variant‑vs‑behaviour extension.
Part 2 arm‑you for messier reality, where both axes mutate at once. Expect visitors, decorators, registries, and middleware chains — with field‑tested heuristics on what to choose when.
🚨 “Can we add tax reporting by end of sprint?”
Friday 4 p.m. A product manager drops a bomb:
“Finance needs a quarterly tax report. It must work on every calculation we’ve ever done — and oh, we’re onboarding crypto swaps next week.”
Two simultaneous shocks:
New behaviour → generate tax data for every computation.
New variant → a fresh CryptoSwap type unknown to today’s code.
Handling one axis is easy (Part 1). Handling both is where designs either shine or shatter.
When behaviours multiply: Reporting, auditing, analytics
Legal, ops, and data teams keep inventing read‑only behaviours — without changing domain logic.
A closed interface plus a visitor (or switch) keeps the blast radius near‑zero.
sealed interface Expr permits Const, Add { }
record Const(int value) implements Expr {}
record Add(Expr l, Expr r) implements Expr {}
interface ExprVisitor<T> {
T visit(Const c);
T visit(Add a);
}
final class AuditVisitor implements ExprVisitor<Void> {
public Void visit(Const c) { log("const " + c.value()); return null; }
public Void visit(Add a) { log("add " + a); return null; }
}
Add AuditVisitor, JsonVisitor, SqlVisitor … no type rewrites required.
When variants explode: Plug‑ins, domain expansion, customer scripts
Tomorrow’s release supports matrices, next quarter brings probability distributions. Copy‑pasting every behaviour into new types is untenable — so flip to an open interface.
export interface Expr { eval(): number }
export class Matrix implements Expr {
constructor(private m: number[][]) {}
eval() { throw new Error("matrix eval TBD") }
}
External packages can ship unknown Expr variants with zero friction.
When both explode: Welcome to the real world
You need composable patterns that isolate change while staying symmetric.
Decorator: Behaviour injection without hierarchy surgery
type Expr interface { Eval() int }
type LoggingExpr struct {
Inner Expr
}
func (l LoggingExpr) Eval() int {
log.Printf("eval %#v", l.Inner)
return l.Inner.Eval()
}
Wrap, don’t rewrite. Works with any future Expr variant.
Dynamic behaviour registry
// behaviourRegistry.ts
type Printer = (e: any) => string;
export const registry = new Map<string, Printer>();
// somewhere in a plug‑in:
registry.set("cryptoSwap", e => `SWAP(${e.asset1}, ${e.asset2})`);
New behaviours attach to new or old types at runtime — perfect for SaaS marketplaces.
Middleware / hooks
type Hook func(Expr) Expr
type Chain []Hook
func (c Chain) Wrap(e Expr) Expr {
for i := len(c)-1; i >= 0; i-- { e = c[i](e) }
return e
}
analytics := func(next Hook) Hook { /* ... */ }
security := func(next Hook) Hook { /* ... */ }
pipe := Chain{analytics, security}
result := pipe.Wrap(expr).Eval()
Compose cross‑cutting concerns like UNIX pipes — arrange, swap, or drop at will.
🧭 Decision Framework
Map volatility: Which axis (behaviour / variant) changes first next quarter?
Start closed if you own the domain (internal product).
Flip open when you invite third‑party extensions (SDK, plug‑ins).
Layer hybrids once both axes heat up — visitor for audits, decorator for runtime hooks, registry for fast plug‑in behaviours.
Re‑evaluate quarterly; architecture is living documentation of change.
Key Takeaways
Designing for one axis is theory; designing for two is practice.
Visitor centralises new behaviours; decorator injects them; registry discovers them.
Close what is stable, open what is volatile, compose when volatility is mutual.
Great software bends with change rather than breaking every sprint.
Up Next (Part 3)
Fragile Base‑Class Antidotes
We’ll tackle inheritance pitfalls and patterns (e.g., Composition‑over‑Inheritance, Extension Objects) that keep large OO trees from collapsing.
Was this useful?
Follow @maneeshchaturvedi for Parts 3 & 4.
Know a teammate battling volatile requirements? Share this post — save them a refactor!
Subscribe to my newsletter
Read articles from Maneesh Chaturvedi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
