Making an API Is Making a Promise

evergreen

When creating an API, one has to define as precisely as possible its inputs, outputs and behaviors.

In particular, for the values, one has to make clear :

Let’s consider you define one function that returns a kind of id. Something like:

def get_id():
	"Returns a unique id"

Imagine now that you want to be very clear about how unique this returned object is (see when to use uuid, uid and id?).

Imagine for example that you want the id to be universally unique. Then you would use UUID to fulfil that promise.

def get_id():
	"Returns a unique id, following RFC 4122 standard"

To me, this is a promise you make to your user. Using your API, your user trusts you to return a RFC 4122 compliant object.

chain of promises

Of course, you are likely to rely on some other API to implement yours. In that case you rely on their promises to make yours.

In the previous example, let’s assume that you created the id using some third party library that is defined like this:

def uuidgen():
   "Return an RFC 4122 compliant UUID version 4"

Then, you can claim that your API returns an UUID. The same way you rely on this uuidgen library, others will rely on you for returning a UUID: this defines a chain of promises.

Of course, that chain may be broken. If at some point uuidgen returns something else than a object compliant with RFC 4122, then your API won’t either. Then you will be legitimate blaming uuidgen, the same way than your users will be legitimate blaming you1.

In general, we assume that the promises are fulfilled. Besides we rely on changelogs to find out whether those promises change.

don’t confuse epistemic claims and promises

Now imagine that instead of using uuidgen, you used another function whose API is:

def id():
   "Return a unique id"

I think you may agree that it is a bad idea to make the promise of returning a UUID in that case. You have no promise to support yours.

Now, imagine that all your previous calls to this function have returned something that looked like an RFC 4122 compliant object. Then, you might be tempted, by induction, to feel confident saying that id returns UUIDs. .

You just made an epistemic claim. You found a reason to believe that the current implementation of id return some UUIDs. But it is still not 100%.

Imagine that during all your life, all the swans you have seen were white. You could be tempted to say that “all swans are white”, until you see a black swan and realize you were wrong. The claim “all swans are white” was still a pretty good claim, in the sense that it was very likely to be right and you had good confidence saying that the next swan you would see would be white.

Now, is this a good enough reason to promise returning a UUID yourself? I don’t think so.

Making a promise is about making sure of something about the future. You basically are promising that your API will behave like that. Making a induction is more making sure of something about the past and draw likely conclusions about the future. You basically are saying that your API may very well behave like that.

In other terms, I think that promising a thing is more engaging than saying that you have good reasons to believe in the thing.

It would be ok in case the API were Bayesians though, you could write something like.

def get_id():
	"Returns a unique id, being a UUID with a prior of 90%"

But, as you may imagine, in API creation, we generally ask about more than just very high probabilities.

what if I am 100% sure?

Now, imagine that you took a look at the code and that it is like this:

def id():
   "Return a uniq id"
   return uuidgen()

Then, you are 100% confident that id returns UUIDs, in its current implementation.

Now, is this a good enough reason to promise returning a UUID yourself? Well, maybe.

Say that you release your product now, people use it and are happy. Now suppose you update your dependency in the future. You checked the changelog and did not see any relevant difference. Then your users complain that your function does not fulfil it promise anymore and returns non RFC 4122 compliant objects.

Say that by looking at the code of id, you realize they changed their implementation to return something else.

Then your users will be legitimate blaming you, as you broke your promise. But you won’t be legitimate blaming id, as no promise was broken.

Therefore, in that case, I would :

  1. either write explicit tests that check that the hypothesis “id return UUID” remains true,
  2. or have a more complicated update checklist in which I would add “take a look at id’s implementation to ascertain it still returns UUIDs”,

2 seems daunting to me, so I would rather do 1.

my opinion

In my opinion, I would:


  1. You can of course create extra checks that uuidgen indeed returns what it is meant to return. This would be some kind of hardcore defensive programming and it totally ok I guess. ↩︎