XRAPH/Research/Technical note

Where Generic Abstraction Stops Helping: Framework Ergonomics in Systems Languages

Two abandoned Rust frameworks, examined for the same failure: an abstraction that re-exports its decisions as type parameters has not hidden them. Proposes counting empty interface implementations as a measurable design signal.

Type
Technical note
Year
2025
Status
Draft
Length
8 min read
Focus area
Distributed Computing

#Abstract

Two microservice frameworks written in Rust were abandoned for the same reason, and the reason was not the borrow checker. This note argues that an abstraction which re-exports its decisions as type parameters has not hidden them, proposes counting empty interface implementations as a measurable design signal, and reports that the author made the same error three times across three languages.

#Observation

Framework code sits at the boundary between the generic and the concrete. In a language where that boundary is expressed in the type signature, the signature accumulates the framework's internal decisions.

In the systems examined, a request handler that resolved two services from a container and returned a response carried four generic parameters, two lifetimes and a trait bound exceeding a line. The author could read it. No user could, and a framework whose signatures only its author can read has failed at the function frameworks exist to perform.

1// Illustrative of the shape rather than a verbatim signature.
2pub async fn handle<S, C, E, R>(
3 req: Request<S::Body>,
4 ctx: &Context<C>,
5) -> Result<Response<R::Body>, E>
6where
7 S: RequestSource + Send + Sync + 'static,
8 C: Container<Error = E> + Clone,
9 E: Into<HandlerError> + From<C::ResolveError> + Send,
10 R: Responder<Context = Context<C>>,
11{ ... }

Every parameter here is doing real work. S exists because the framework supports more than one transport. C exists because the container is pluggable. E exists because error types compose across both. R exists because response construction was made extensible. Each was added for a reason a reviewer would accept in isolation, which is how the signature reached this state without anyone making a bad decision.

#How the parameters accumulate

The mechanism is worth stating because it is not carelessness and it will recur in any similar project.

A framework has variation points, which are the places where a user might reasonably want different behaviour. In a language where variation is expressed by static dispatch, each variation point becomes a type parameter on every function that touches it, and parameters propagate outward along the call graph until they reach the user. Nothing bounds this. Adding a fifth variation point is a small local change, and its cost is a fifth parameter on the signature every user writes.

Two properties make the accumulation hard to notice from inside. It is incremental, so no single commit makes the signature bad. And it is invisible to the author, who knows what each parameter means and reads the signature as five familiar things rather than as one unfamiliar thing.

The parameters are not the mistake. Continuing to add them after the signature stopped being readable is the mistake, and nothing in the process signals when that happened.

#What the alternative costs

The obvious response is dynamic dispatch: erase the variation into a trait object or an interface value and let the signature stay small. That is what the third framework did and it is what I would do again, and it is not free.

Errors that were compile-time become runtime. A container that cannot resolve a service is now a failure at first request rather than a type error, and the compiler's help is real, as noted below. There is an indirection cost per call, usually negligible and occasionally not. And some genuinely useful static guarantees disappear, including the parametricity properties that let one reason about a generic function from its type alone .

The trade is therefore real rather than obvious, and the position taken here is narrow: where the code is extended by users rather than merely used by them, the cost of an unreadable signature is paid by many people repeatedly, and the cost of a runtime resolution error is paid once by the framework author writing a good error message.

#The criterion being violated

Parnas states that a module should be characterised by the design decision it hides . A type parameter for every internal variation point does the opposite: it makes the decision visible in the signature and requires every user to satisfy it.

Brooks separates the essential difficulty of a problem from the accidental difficulty introduced by tools and representation . Distributed service lifecycle is essential. Four generic parameters to express a request handler is accidental, and eighteen months were spent on the accidental part.

#A measurable signal

Direct measurement of abstraction quality is difficult. This note proposes an indirect one that is easy to compute and, in the author's experience, predictive.

E(I)  =  1MPmMpP1[p.m is trivial]E(I) \;=\; \frac{1}{|M| \cdot |P|} \sum_{m \in M} \sum_{p \in P} \mathbb{1}\left[\, p.m \text{ is trivial} \,\right]
(1)
Emptiness ratio over an interface

where M is the interface's required methods and P its implementations. A trivial implementation is one returning a zero value with no other effect.

The claim is that a high emptiness ratio indicates the interface has bundled independent concerns, and that the correct response is to split it into one required method plus optional capabilities detected at runtime. An implementation that then declares an optional capability is communicating something, whereas an empty required method communicates nothing.

1Plugin interface, seven required methods, five implementations
2
3 Init Start Stop Config Health Metrics Migrate empty
4metrics . . . . . . X 1/7
5tracing . . . . . X X 2/7
6audit . . . X X X . 3/7
7featureflag . . X . X X X 4/7
8cache . . . . . X X 2/7
9 E(I) = 0.34

The pattern in the table is the diagnostic, not the number. Init and Start are implemented by everything, so they belong in the required interface. Metrics and Migrate are trivial in four of five, so they are not obligations of being a plugin; they are things some plugins do. An interface demanding both from every implementer has asserted that all plugins are alike in seven ways when they are alike in two.

Three things the ratio does not capture are worth naming, since a metric offered without its blind spots invites misuse. It says nothing about whether the two genuinely required methods are the right two. It treats a method that is empty because the concern does not apply the same as one that is empty because the implementer has not got to it yet, and those are opposite situations. And it is trivially gamed, since an implementer who writes a plausible-looking body instead of returning a zero value moves the number without changing anything.

#Evidence

Three frameworks by the same author shipped an interface with seven required methods. In each case the majority of implementations satisfied the majority of methods trivially. In the third case the interface was reduced to one required method and the emptiness ratio fell to zero by construction, since optional capabilities are only implemented when used.

This is a single author across three projects, which is the weakest form of evidence. It is reported because the repetition across three languages suggests the error is not obvious, and organisational and representational factors of this kind are known to shape the artefacts that result .

#What the third framework did instead

Describing an error without describing the correction leaves a reader with nothing to act on, so here is what the third attempt looks like and where it is still unsatisfactory.

Variation points are resolved at construction rather than in signatures. The container, the transport and the error mapping are chosen when the application is built and are held behind interface values, so a handler signature mentions the request, the response and nothing else. A user writing a handler sees an interface with two types in it, both of which are theirs.

Optional behaviour uses detected capabilities rather than type parameters, which is the construction treated at length in a companion note. The relevant property here is that it moves optionality out of the signature: a component that supports an extra behaviour implements an extra interface, and a component that does not is unaffected and unaware.

And extension points are added reluctantly. The specific discipline that helped was requiring a second real use case before a variation point is introduced, on the reasoning that one use case is a requirement and two is a pattern. Several parameters in the earlier frameworks existed because a single hypothetical user might have wanted them.

Two things remain unsatisfactory. Errors that the earlier design caught at compile time are now caught at startup, and while startup is early enough in practice it is not the same guarantee. And the discipline about extension points is a discipline, which puts it in the same category as remembering a predicate: it depends on the author holding a line, and nothing enforces it. A framework three years older than the one described will be a good test of whether the line held.

#Scope of the claim

This is not an argument against expressive type systems. The compiler correctly identified real lifetime errors in a service registry that would otherwise have appeared under concurrency, which is a genuine benefit.

The argument is narrower: for code that is extended by users rather than merely used by them, decisions surfaced in signatures are decisions transferred to users. A gateway with no user-authored handlers in its signatures does not have this problem, which is why the author continues to use the same language for that class of program.

#Limitations

The emptiness ratio has not been validated against any independent measure of design quality. It is proposed as a cheap heuristic with a plausible mechanism, and testing it would require classifying interfaces across a corpus of frameworks by an independent quality judgement, which has not been done. That study is feasible: interface definitions and their implementations are extractable from open source at scale, and the ratio could be correlated against something like the rate of interface-breaking changes over a project's history. I would find the result interesting either way and have not done it.

No threshold is offered. The table above shows 0.34 and I do not claim that 0.3 is bad and 0.2 is acceptable. In the cases examined the interfaces that felt wrong were well above 0.3 and the ones that felt right were near zero, which is the sort of statement that should make a reader suspicious of the whole exercise, and is why it is offered as a signal to look at rather than as a gate.

The three-frameworks evidence is one author repeating one error, which establishes that the error is easy to make and nothing about how common it is generally. Somebody else making a different error three times would report a different heuristic with equal conviction.

Language features have changed since the systems described were written, and a version written today would face a weaker form of the same constraint. The mechanism by which parameters propagate along the call graph has not changed, so I expect the phenomenon to persist in milder form, and I have not rebuilt either framework on a current toolchain to check.

References

  1. [1]Philip Wadler, Theorems for Free!, Conference on Functional Programming Languages and Computer Architecture, 1989doi:10.1145/99370.99404
  2. [2]Luca Cardelli, Peter Wegner, On Understanding Types, Data Abstraction, and Polymorphism, ACM Computing Surveys, vol. 17, no. 4, pp. 471-523, 1985doi:10.1145/6041.6042
  3. [3]D. L. Parnas, On the Criteria To Be Used in Decomposing Systems into Modules, Communications of the ACM, vol. 15, no. 12, pp. 1053-1058, 1972doi:10.1145/361598.361623
  4. [4]Frederick P. Brooks Jr., No Silver Bullet: Essence and Accidents of Software Engineering, Computer, vol. 20, no. 4, pp. 10-19, 1987doi:10.1109/MC.1987.1663532
  5. [5]Melvin E. Conway, How Do Committees Invent?, Datamation, vol. 14, no. 5, pp. 28-31, 1968