Make Dangerous Flexibility Explicit

Apr 20, 2023
Harden Ship

Abstraction should not hide the limits operators need to see.

A flexible interface is not automatically a better interface.

Flexibility moves decisions into runtime. That may be useful in application code where one implementation owns validation and mistakes are reversible.

It is more dangerous when the operation can destroy data, move money, weaken authorization, or create incompatible behavior across implementations.

In those cases, I prefer interfaces that make the important constraint visible.

A Small Low-Level Example

The Ethereum Virtual Machine exposes five logging instructions:

LOG0
LOG1
LOG2
LOG3
LOG4

The number states how many topics the operation consumes.

A generic interface could have been designed instead:

LOG(offset, length, topic_count, ...)

That version looks more compact. It also moves more behavior into runtime interpretation:

  • validate the count
  • determine the stack shape
  • reject malformed combinations
  • preserve identical interpretation across clients

The explicit instruction family makes the valid range and operand shape visible before execution.

The point is not that repeated APIs are always superior.

The point is that abstraction should not hide the exact constraint that makes misuse expensive.

Different Layers Need Different Interfaces

At an application SDK boundary, this may be appropriate:

emitLog({ topics, data });

The SDK can validate topic count and expose a convenient typed interface.

At a consensus execution boundary, explicit instructions reduce ambiguity across independent implementations.

The correct design depends on where being wrong becomes expensive.

Operational Interfaces

Consider two cleanup APIs:

cleanup(options);

and:

previewExpiredRecords();
deletePreviewedRecords(batchId);

The first is compact and extensible.

The second exposes a critical operational boundary: selection and deletion are separate actions.

That separation enables dry runs, evidence, review, batch limits, and policy checks before mutation.

For a reversible low-impact operation, the generic interface may be enough.

For destructive production work, the explicit workflow is easier to contain and audit.

Abstraction Moves Complexity

Abstraction does not remove complexity. It decides where complexity lives.

A flexible interface may move it into:

  • implicit defaults
  • runtime validation
  • parser behavior
  • configuration combinations
  • error handling
  • operational debugging

That tradeoff is acceptable only when the receiving layer can own those obligations reliably.

Design Test

Before choosing a flexible interface, ask:

  1. Which constraints must remain visible to the caller?
  2. Which decisions can safely happen at runtime?
  3. What happens when the input is malformed?
  4. Is the failure reversible?
  5. Must multiple implementations agree exactly?
  6. Does the abstraction reduce misuse or hide important limits?
  7. Which layer will own validation, evidence, and recovery?

A clean interface is not the one with the fewest functions.

It is the one that puts ambiguity in the layer best able to contain it.

Comments