In this story
An interface is where one piece of software makes a promise to another. The promise includes the happy path, but it also includes what happens when the input is missing, the work fails, or the caller asks twice.
A function signature is a start, but it’s the behavior around it that callers actually rely on.
Say what a valid input means
Consider an illustrative endpoint that accepts a duration in milliseconds. A numeric field alone leaves several questions open. Can it be negative? Does zero mean no wait or no timeout? Are fractional values accepted? What happens if a caller sends infinity?
Those choices belong near the boundary. If the implementation silently makes different assumptions in several places, the contract becomes whatever the caller happens to encounter first.
For that duration endpoint, a sensible contract might accept only finite, non-negative values. A slider can keep routine input within that range, but validation at the endpoint still matters when another client calls it directly.
Make failure part of the output
A missing record, an unavailable dependency, and malformed input mean different things. If they all collapse into an empty response, the caller has to guess whether to retry, ask for clarification, or stop.
Use a small set of failures the caller can act on. Keep internal details out of public error messages while preserving diagnostic context in the appropriate logs.
Test behavior across the boundary
A test that just repeats the implementation will happily agree with the same mistake. Write checks around the promise instead: invalid input is rejected, an empty collection remains valid, a repeated operation has the intended effect, and callers can distinguish the outcomes they need.
For a parallel timing calculation, a useful invariant is that overlapping independent work cannot take longer than executing the same fixed work sequentially under the model’s assumptions. That describes behavior without prescribing every line of code.
Let implementation change
Once callers depend on a clear contract, the implementation can evolve behind it. A faster algorithm or different storage layer is easier to introduce when you can tell whether the visible behavior stayed valid.
Aim for a boundary that’s small enough to understand and clear enough that nobody has to read the whole system to use it.
