Beyond interfaces and polymorphism, what concrete mechanisms can be used to achieve Protected Variations, and what are the trade-offs of each?
answer
- not just interfaces: data, config, events, lookup
- adapter/ACL for external systems
- axis: compile-time → runtime, code → data
- flexibility bought with lost static checking
- least powerful mechanism that fits
basics
~20 sInterfaces are only one option. You can also move variation into data or configuration, use an adapter or facade around an external system, look implementations up at runtime, publish events instead of calling recipients, or adopt a standard format so vendors are interchangeable.
solid answer
~50 sProtected Variations names the goal, not the technique. Common mechanisms: **encapsulation** (hide representation), **polymorphism/interfaces** (hide which algorithm), **indirection** — adapter, facade, proxy, anti-corruption layer (hide an external or legacy system), **data-driven design** (rules, mappings and rates in tables or config, so change means editing data not code), **service lookup / dependency injection** (hide *which* implementation and where it comes from), **interpreters and rules engines** (logic as data, changeable at runtime), **events / publish-subscribe** (hide *who* reacts, so adding a consumer doesn't touch the producer), **standard formats and protocols** (hide vendor identity), and the **Law of Demeter** (hide object-graph structure). Trade-offs run along one axis: the more the variation moves out of code, the more flexible and the less statically checkable it becomes. Data- and interpreter-driven designs buy runtime change at the price of a new mini-language, weak tooling, and harder debugging.
code
pseudocode · 15 lines// Same variation point (shipping cost), three protection mechanisms.
// 1) Polymorphism — variation lives in code, checked at compile time.
interface ShippingPolicy: cost(order) -> Money
// 2) Data-driven — variation lives in a table, editable without deploy.
// region | weight_max | price
// EU | 2.0 | 4.99
cost(order) = rateTable.lookup(order.region, order.weight)
// 3) Rules/interpreter — variation authored at runtime as an expression.
cost(order) = ruleEngine.eval("weight>2 ? base*1.5 : base", order)
// Flexibility increases 1 -> 3; compile-time safety, tooling and
// debuggability decrease 1 -> 3. Pick the leftmost that actually fits.go deeper
Name three or four mechanisms beyond interfaces — configuration, an adapter around an external API, dependency injection — with one example each.
Give the catalogue and pair each mechanism with the kind of variation it protects against, plus one honest downside.
Articulate the single axis (compile-time → runtime, code → data) and argue for the least powerful mechanism that meets the demonstrated need for change.
Add ownership and economics: who authors the variation, how often, on what release cadence, and what compatibility policy the resulting extension surface commits the organisation to for years.
## Framing **Protected Variations (PV)** says: identify predicted points of variation or instability and surround them with a **stable interface** — something clients bind to that changes far less often than what it hides. The word "interface" is used loosely: a table schema, a config key, a message contract and a plugin registry are all interfaces in this sense. ## The mechanism catalogue ### 1. Encapsulation / information hiding Hide the internal representation behind operations. Protects against: changes to data structures and storage layout. *Trade-off:* almost free; the risk is fake encapsulation (getters/setters that expose the representation anyway). ### 2. Polymorphism behind an interface A family of implementations behind one contract. Protects against: variation in *behaviour*. *Trade-off:* the contract must be the **union of what clients need** and the **intersection of what implementations can promise**. When those don't overlap cleanly you get a lowest-common-denominator interface, or capability flags (`supportsPartialRefund()`) that leak variation back to callers. ### 3. Indirection: adapter, facade, proxy, anti-corruption layer A local object stands in for an external/legacy/vendor system and translates vocabulary. *Trade-off:* the strongest protection at system boundaries, but you now own a translation layer that must be maintained and can drift from both sides. Also risks *pass-through* layers that translate nothing. ### 4. Data-driven design Move the varying part into data: rate tables, feature flags, mapping files, property files, database rows, templates. *Trade-off:* change without redeploying; but you lose type checking, refactoring support and compile-time errors, and you gain a config surface that can be wrong in production. Data also needs versioning, validation and review — otherwise "config" becomes untested code. ### 5. Service lookup / dependency injection / plugin registries Clients ask a registry (or are handed) an implementation, so *which* implementation and *where it lives* are both hidden. *Trade-off:* wiring becomes runtime, not compile-time — misconfiguration surfaces late; navigability suffers ("who implements this?" is no longer a static question). ### 6. Interpreters, rule engines, DSLs, metadata-driven designs Logic itself becomes data that a general engine executes. *Trade-off:* maximum runtime flexibility, maximum cost. You have invented a language: it needs syntax, errors, tests, tooling, debugging and people who know it. Justified when non-developers must change rules frequently, rarely otherwise. ### 7. Events / publish–subscribe The producer announces a fact; consumers subscribe. Protects the producer against variation in **who cares**. *Trade-off:* adding a consumer no longer touches the producer, but control flow becomes implicit; ordering, delivery guarantees, and "what happens overall" become hard to see. Debugging shifts from stack traces to correlation IDs. ### 8. Standard formats, protocols and languages Speaking a standard (a common wire format, a query language, a portable spec) makes vendors substitutable by construction. *Trade-off:* you are protected only to the extent implementations don't extend the standard — and they always extend the standard. Using an extension quietly re-couples you. ### 9. Law of Demeter ("don't talk to strangers") Call only your immediate collaborators; don't navigate `a.getB().getC().getD()`. Protects against variation in the **shape of the object graph**. *Trade-off:* forwarding methods multiply; applied dogmatically it produces bloated pass-through APIs. It also does not apply to pure data structures / DTOs. ### 10. Versioned contracts and tolerant readers At system scale: additive-only schema evolution, ignoring unknown fields, explicit version negotiation. *Trade-off:* protects both sides during independent deployment, at the cost of accumulating compatibility rules and dead fields nobody dares delete. ## The single axis behind all trade-offs Every mechanism moves the variation **later in time and further from the compiler**: `hardcoded → conditional in code → polymorphic type → injected implementation → config data → interpreted rules → externally authored plugin` Moving right buys flexibility (change without editing code, without redeploying, without a developer) and pays with lost static checking, weaker tooling, harder debugging, and more ways to be wrong at runtime. Choose the **leftmost** point that satisfies the actual, demonstrated need for change. "Who needs to change this, how often, and how fast?" decides the position far better than aesthetics. ## Choosing well - Variation among a fixed, known, code-owned set → **polymorphism**. - Variation owned by business people, changing weekly → **data/config**. - Variation owned by a third party → **adapter / anti-corruption layer**. - Variation in *who reacts to something* → **events**. - Variation in deployment/environment → **injection + configuration**. - Variation you cannot enumerate at all, authored by outsiders → **plugin/extension point** (highest cost — needs a stable public API and a compatibility policy).
- When would you deliberately choose data-driven configuration over polymorphism for a variation point?When the change is frequent, owned by non-developers, or must take effect without a deploy — pricing tables, tax rates, feature toggles, content. Polymorphism is better when the variants are few, developer-owned, and benefit from type checking and refactoring tools.
- How do events protect a producer, and what do you give up?The producer states a fact and never learns who consumes it, so new consumers are added without touching it — protection against variation in *who cares*. You give up explicit control flow: ordering, retries, delivery semantics and end-to-end traceability all become harder, and failures surface far from the cause.
saying these in an interview costs you the question
- Believing Protected Variations can only be achieved with interfaces and inheritance.
- Reaching for a rules engine or DSL for a variation that changes twice a year and is edited only by developers — huge cost for flexibility nobody uses.
- Treating configuration as free: untested, unversioned, unvalidated config is production code with no compiler and no review.
- Assuming a standard format or protocol guarantees vendor portability, while quietly relying on a vendor-specific extension.
- Applying the Law of Demeter mechanically to data-transfer objects, producing pointless forwarding methods.
- Calling a pass-through wrapper that adds no translation an anti-corruption layer.