1
0
mirror of https://github.com/twirl/The-API-Book.git synced 2025-01-23 17:53:04 +02:00

style fixes

This commit is contained in:
Sergey Konstantinov 2022-05-06 01:09:48 +03:00
parent 2e9e741d35
commit 0ad3749f52
8 changed files with 237 additions and 238 deletions

View File

@ -1,100 +0,0 @@
### The Backwards Compatibility Problem Statement
As usual, let's conceptually define ‘backwards compatibility’ before we start.
Backwards compatibility is a feature of the entire API system to be stable in time. It means the following: **the code which developers have written using your API, continues working functionally correctly for a long period of time**. There are two important questions to this definition, and two explanations:
1. What does ‘functionally correctly’ mean?
It means that the code continues to serve its function, e.g. solve some users' problem. It doesn't mean it continues working indistinguishably: for example, if you're maintaining a UI library, changing functionally insignificant design details like shadow depth or border stoke type is backwards compatible, whereas changing visual components size is not.
2. What does ‘a long period of time’ mean?
From our point of view, backwards compatibility maintenance period should be reconciled with subject area applications lifetime. Platform LTS periods are a decent guidance in the most cases. Since apps will be rewritten anyway when the platform maintenance period ends, it is reasonable to expect developers to move to the new API version also. In mainstream subject areas (e.g. desktop and mobile operating systems) this period lasts several years.
From the definition becomes obvious why backwards compatibility needs to be maintained (including taking necessary measures at the API design stage). An outage, full or partial, caused by the API vendor, is an extremely uncomfortable situation for every developer, if not a disaster — especially if they pay money for the API usage.
But let's take a look at the problem from another angle: why the maintaining backwards compatibility problem exists at all? Why would anyone *want* to break at? This question, though it looks quite trivial, is much more complicated than the previous one.
We could say the *we break backwards compatibility to introduce new features to the API*. But that would be deceiving: new features are called *‘new’* just because they cannot affect existing implementations which are not using them. We must admit there are several associated problems, which lead to the aspiration to rewrite *our* code, the code of the API itself, and ship new major version:
* the code eventually becomes outdated; making changes, even introducing totally new functionality, is impractical;
* the old interfaces aren't suited to encompass new features; we would love to extend existing entities with new properties, but simply couldn't;
* finally, with years passing since the initial release, we understood more about the subject area and API usage best practices, and we would implement many things differently.
These arguments could be summarized frankly as ‘the API developers don't want to support the old code’. But this explanation is still incomplete: even if you're not going to rewrite the API code to add new functionality, or you're not going to add it at all, you still have to ship new API versions, minor and major alike.
**NB**: in this chapter we don't make any difference between minor versions and patches: ‘minor version’ means any backwards compatible API release.
Let us remind that [an API is a bridge]((https://twirl.github.io/The-API-Book/docs/API.en.html#chapter-2)), a meaning of connecting different programmable contexts. No matter how strong our desire to keep the bridge intact is, for our capabilities are limited: we could lock the bridge, but we cannot command the rifts and the canyon itself. That's the source of the problems: we can't guarantee *our own* code wouldn't change, so at some point we will have to ask the clients to change *their* code.
Apart from our own aspirations to change the API architecture, three other tectonic processes are happening at the same time: user agents, subject areas, and underlying platforms erosion.
#### Consumer applications fragmentation
When you shipped the very first API version, and first clients started to use it, the situation was perfect. There was only one version, and all clients were using just it. When this perfection ends, two scenarios are possible.
1. If the platform allows for fetching code on-demand, like the good old Web does, and you weren't too lazy to implement that code-on-demand (in a form of a platform SDK — for example, JS API), then the evolution of your API is more or less under your control. Maintaining backwards compatibility effectively means keeping *the client library* backwards compatible. As for client-server interaction, you're free.
It doesn't mean that you can't break backwards compatibility. You still can make a mess with cache control headers or just overlook a bug in the code. Besides, even code-on-demand systems don't get updated instantly. The author of this book faced the situation, when users were deliberately keeping a browser tab open *for weeks* to get rid of updates. But still, you usually don't have to support more than two API versions — the last one and the penultimate one. Furthermore, you may try to rewrite previous major version of the library, implementing it upon the actual version API.
2. If code-on-demand isn't supported or is prohibited by the platform, as in modern mobile operating systems, then the situation becomes more severe. Each client effectively borrows a snapshot of the code, working with your API, frozen at the moment of compilation. Client application updates are scattered over time at much more extent than Web application updates. The most painful thing is that *some clients will never be up to date*, because of one of three reasons:
* developers simply don't want to update the app, its development stopped;
* users don't wont to get updates (sometimes because users think that developers ‘spoiled’ the app in new versions);
* users can't get updates because their devices are no longer supported.
In modern times these three categories combined could easily constitute tens of percent of auditory. It implies that cutting the support of any API version might be remarkable — especially if developers' apps continue supporting a more broad spectrum of platforms than the API does.
You could have never issued any SDK, providing just the server-side API, for example in a form of HTTP endpoints. You might think, given your API is less competitive on the market because of a lack of SDKs, that the backwards compatibility problem is mitigated. That's not true: if you don't provide an SDK, then developers will either adopt an unofficial one (if someone bothers to make it), or just write a framework of themselves — independently. ‘Your framework — your problems’ strategy, fortunately or not, works badly: if developers write poor quality code upon your API, then your API is of a poor quality itself. Definitely in the view of developers, possibly in the view of end users, if the API performance within the app is visible to them.
Certainly, if you provide a stateless API which doesn't need client SDKs (or they might be auto-generated from the spec), those problems will be much less noticeable, but not fully avoidable, unless you never issue any new API version. Otherwise you still had to deal with some distribution of users by API and SDK versions.
#### Subject area evolution
The other side of the canyon is the underlying functionality you're exposing via the API. It's, of course, not static and somehow evolves:
* new functionality emerges;
* older functionality shuts down;
* interfaces change.
As usual, the API provides an abstraction to much more granular subject area. In case of our [coffee machine API example](https://twirl.github.io/The-API-Book/docs/API.en.html#chapter-7) one might reasonably expect new models to pop up, which are to be supported by the platform. New models tend to provide new APIs, and it's hard to guarantee they might be included preserving the same high-level API. And anyway, the code needs to be altered, which might lead to incompatibility, albeit unintentional.
Let us also stress that low-level API vendors are not always as resolute regarding maintaining backwards compatibility (actually, any software they provide) as (we hope so) you are. You should be prepared that keeping your API in an operational state, e.g. writing and supporting facades to the shifting subject area landscape, will be your problem, and rather a sudden one.
#### Platform drift
Finally, there is a third side to a story — the ‘canyon’ you're crossing over with a bridge of your API. Developers write code which is executed in some environment you can't control, and it's evolving. New versions of operating system, browsers, protocols, language SDKs emerge. New standards are being developed, new arrangements made, some of them being backwards incompatible, and nothing could be done about that.
Older platform versions lead to the fragmentation, just like older apps versions do, because developers (including the API developers) are struggling with supporting older platforms, and users are struggling with platform updates — and often can't update at all, since newer platform versions require newer devices.
The nastiest thing here is that not only incremental progress in a form of new platforms and protocols demands changing the API, but also a vulgar fashion does. Several years ago realistic 3d icons were popular, but since then the public taste changed in a favor of flat and abstract ones. UI components developers had to follow the fashion, rebuilding their libraries, either shipping new icons or replacing old ones. Similarly, right now ‘night mode’ support is introduced everywhere, demanding changes in a broad range of APIs.
#### Backwards compatibility policy
To summarize the above:
* you will have to deploy new API versions because of apps, platforms, and subject area evolution; different areas are evolving with different rate, but never a zero one;
* that will lead to fragmenting the API versions usage over different platforms and apps;
* you have to make decisions critically important to your API's sustainability in the customers view.
Let's briefly describe these decisions and key factors of making them.
1. How often new major API versions should be developed?
That's primarily a *product* question. New major API version is to be released when the critical mass of functionality is achieved — a critical mass of features which couldn't be introduced in the previous API versions, or introducing them is too expensive. On stable markets such a situation occurs once in several years, usually. On emerging markets new API major versions might be shipped more frequently, only depending on your capabilities of supporting the zoo of previous versions. However, we should note that deploying a new version before the previous one was stabilized (which commonly takes from several months up to a year) is always a troubling sign to developers, meaning they're risking dealing with the unfinished platform glitches permanently.
2. How many *major* versions should be supported at a time?
As for major versions, we gave *theoretical* advice earlier: ideally, the major API version lifecycle should be a bit longer than the platform's one. In stable niches like desktop operating systems it constitutes 5 to 10 years. In new and emerging ones it is less, but still measures in years. *Practically* speaking you should look at the size of auditory which continues using older versions.
3. How many *minor* versions (within one major version) should be supported at a time?
As for minor versions, there are two options:
* if you provide server-side APIs and compiled SDKs only, you may basically do not expose minor versions at all, just the actual one: the server-side API is totally within your control, and you may fix any problem efficiently;
* if you provide code-on-demand SDKs, it is considered a good form to provide an access to previous minor versions of SDK for a period of time sufficient enough for developers to test their application and fix some issues if necessary. Since full rewriting isn't necessary, it's fine to align with apps release cycle duration in your industry, which is usually several months in worst cases.
We will address these questions in more details in the next chapters. Additionally, in the Section III we will also discuss, how to communicate to customers about new versions releases and older versions support discontinuance, and how to stimulate them to adopt new API versions.

View File

@ -0,0 +1,99 @@
### The Backwards Compatibility Problem Statement
As usual, let's conceptually define ‘backwards compatibility’ before we start.
Backwards compatibility is a feature of the entire API system to be stable in time. It means the following: **the code which developers have written using your API, continues working functionally correctly for a long period of time**. There are two important questions to this definition and two explanations:
1. What does ‘functionally correctly’ mean?
It means that the code continues to serve its function, e.g. solve some users' problems. It doesn't mean it continues working indistinguishably: for example, if you're maintaining a UI library, changing functionally insignificant design details like shadow depth or border stoke type is backwards compatible, whereas changing visual components size is not.
2. What does ‘a long period of time’ mean?
From our point of view, the backwards compatibility maintenance period should be reconciled with the subject area application lifetime. Platform LTS periods are decent guidance in most cases. Since apps will be rewritten anyway when the platform maintenance period ends, it is reasonable to expect developers to move to the new API version also. In mainstream subject areas (e.g. desktop and mobile operating systems) this period lasts several years.
From the definition becomes obvious why backwards compatibility needs to be maintained (including taking necessary measures at the API design stage). An outage, full or partial, caused by the API vendor, is an extremely uncomfortable situation for every developer, if not a disaster — especially if they pay money for the API usage.
But let's take a look at the problem from another angle: why the maintaining backwards compatibility problem exists at all? Why would anyone *want* to break it? This question, though it looks quite trivial, is much more complicated than the previous one.
We could say the *we break backwards compatibility to introduce new features to the API*. But that would be deceiving: new features are called *‘new’* just because they cannot affect existing implementations which are not using them. We must admit there are several associated problems, which lead to the aspiration to rewrite *our* code, the code of the API itself, and ship a new major version:
* the code eventually becomes outdated; making changes, even introducing totally new functionality, is impractical;
* the old interfaces aren't suited to encompass new features; we would love to extend existing entities with new properties, but simply couldn't;
* finally, with years passing since the initial release, we understood more about the subject area and API usage best practices, and we would implement many things differently.
These arguments could be summarized frankly as ‘the API developers don't want to support the old code’. But this explanation is still incomplete: even if you're not going to rewrite the API code to add new functionality, or you're not going to add it at all, you still have to ship new API versions, minor and major alike.
**NB**: in this chapter, we don't make any difference between minor versions and patches: ‘minor version’ means any backwards-compatible API release.
Let us remind that [an API is a bridge]((https://twirl.github.io/The-API-Book/docs/API.en.html#chapter-2)), a meaning of connecting different programmable contexts. No matter how strong our desire to keep the bridge intact is, our capabilities are limited: we could lock the bridge, but we cannot command the rifts and the canyon itself. That's the source of the problems: we can't guarantee that *our own* code won't change, so at some point, we will have to ask the clients to change *their* code.
Apart from our aspirations to change the API architecture, three other tectonic processes are happening at the same time: user agents, subject areas, and underlying platforms erosion.
#### Consumer applications fragmentation
When you shipped the very first API version, and the first clients started to use it, the situation was perfect. There was only one version, and all clients were using just it. When this perfection ends, two scenarios are possible.
1. If the platform allows for fetching code on-demand as the good old Web does, and you weren't too lazy to implement that code-on-demand feature (in a form of a platform SDK — for example, JS API), then the evolution of your API is more or less under your control. Maintaining backwards compatibility effectively means keeping *the client library* backwards-compatible. As for client-server interaction, you're free.
It doesn't mean that you can't break backwards compatibility. You still can make a mess with cache-control headers or just overlook a bug in the code. Besides, even code-on-demand systems don't get updated instantly. The author of this book faced the situation, when users were deliberately keeping a browser tab open *for weeks* to get rid of updates. But still, you usually don't have to support more than two API versions — the last one and the penultimate one. Furthermore, you may try to rewrite the previous major version of the library, implementing it on top of the actual API version.
2. If the code-on-demand feature isn't supported or is prohibited by the platform, as in modern mobile operating systems, then the situation becomes more severe. Each client effectively borrows a snapshot of the code, working with your API, frozen at the moment of compilation. Client application updates are scattered over time at much more extent than Web application updates. The most painful thing is that *some clients will never be up to date*, because of one of the three reasons:
* developers simply don't want to update the app, e.g. its development stopped;
* users don't want to get updates (sometimes because users think that developers ‘spoiled’ the app in new versions);
* users can't get updates because their devices are no longer supported.
In modern times these three categories combined could easily constitute tens of percent of auditory. It implies that cutting the support of any API version might be remarkable — especially if developers' apps continue supporting a more broad spectrum of platforms than the API does.
You could have never issued any SDK, providing just the server-side API, for example in a form of HTTP endpoints. You might think, given your API is less competitive on the market because of a lack of SDKs, that the backwards compatibility problem is mitigated. That's not true: if you don't provide an SDK, then developers will either adopt an unofficial one (if someone bothers to make it) or just write a framework themselves — independently. ‘Your framework — your problems’ strategy, fortunately or not, works badly: if developers write poor quality code upon your API, then your API is of poor quality itself. Definitely in the view of developers, possibly in the view of end-users, if the API performance within the app is visible to them.
Certainly, if you provide a stateless API that doesn't require client SDKs (or they might be auto-generated from the spec), those problems will be much less noticeable, but not fully avoidable, unless you never issue any new API version. If you do, you will still have to deal with some fragmentation of users by API and SDK versions.
#### Subject area evolution
The other side of the canyon is the underlying functionality you're exposing via the API. It's, of course, not static and somehow evolves:
* new functionality emerges;
* older functionality shuts down;
* interfaces change.
As usual, the API provides an abstraction to a much more granular subject area. In the case of our [coffee machine API example](https://twirl.github.io/The-API-Book/docs/API.en.html#chapter-7) one might reasonably expect new models to pop up, which are to be supported by the platform. New models tend to provide new APIs, and it's hard to guarantee they might be adopted while preserving the same high-level API. And anyway, the code needs to be altered, which might lead to incompatibility, albeit unintentional.
Let us also stress that low-level API vendors are not always as resolute regarding maintaining backwards compatibility for their APIs (actually, any software they provide) as (we hope so) you are. You should be warned that keeping your API in an operational state, e.g. writing and supporting facades to the shifting subject area landscape, will be your problem, and rather a sudden one.
#### Platform drift
Finally, there is a third side to a story — the ‘canyon’ you're crossing over with a bridge of your API. Developers write code that is executed in some environment you can't control, and it's evolving. New versions of operating systems, browsers, protocols, programming language SDKs emerge. New standards are being developed, new arrangements made, some of them being backwards-incompatible, and nothing could be done about that.
Older platform versions lead to fragmentation just like older app versions do, because developers (including the API developers) are struggling with supporting older platforms, and users are struggling with platform updates — and often can't update at all, since newer platform versions require newer devices.
The nastiest thing here is that not only does incremental progress in a form of new platforms and protocols demand changing the API, but also does a vulgar fashion. Several years ago realistic 3d icons were popular, but since then the public taste changed in a favor of flat and abstract ones. UI components developers had to follow the fashion, rebuilding their libraries, either shipping new icons or replacing old ones. Similarly, right now ‘night mode’ support is introduced everywhere, demanding changes in a broad range of APIs.
#### Backwards compatibility policy
To summarize the above:
* you will have to deploy new API versions because of apps, platforms, and subject area evolution; different areas are evolving at a different pace, but never stop doing so;
* that will lead to fragmenting the API versions usage over different platforms and apps;
* you have to make decisions critically important to your API's sustainability in the customers' view.
Let's briefly describe these decisions and the key factors for making them.
1. How often new major API versions should be developed?
That's primarily a *product* question. A new major API version is to be released when the critical mass of functionality is reached — a critical mass of features that couldn't be introduced in the previous API versions, or introducing them is too expensive. In stable markets, such a situation occurs once in several years, usually. In emerging markets, new API major versions might be shipped more frequently, only depending on your capabilities of supporting the zoo of previous versions. However, we should note that deploying a new version before the previous one was stabilized (which commonly takes from several months up to a year) is always a troubling sign to developers, meaning they're risking dealing with the unfinished platform glitches permanently.
2. How many *major* versions should be supported at a time?
As for major versions, we gave *theoretical* advice earlier: ideally, the major API version lifecycle should be a bit longer than the platform's one. In stable niches like desktop operating systems, it constitutes 5 to 10 years. In new and emerging ones, it is less but still measured in years. *Practically* speaking you should look at the size of auditory which continues using older versions.
3. How many *minor* versions (within one major version) should be supported at a time?
As for minor versions, there are two options:
* if you provide server-side APIs and compiled SDKs only, you may basically do not expose minor versions at all, just the actual one: the server-side API is totally within your control, and you may fix any problem efficiently;
* if you provide code-on-demand SDKs, it is considered a good form to provide an access to previous minor versions of SDK for a period of time sufficient enough for developers to test their application and fix some issues if necessary. Since full rewriting isn't necessary, it's fine to align with apps release cycle duration in your industry, which is usually several months in worst cases.
We will address these questions in more detail in the next chapters. Additionally, in the Section III we will also discuss, how to communicate to customers about new releases and older versions support discontinuance, and how to stimulate them to adopt new API versions.

View File

@ -1,139 +1,139 @@
### On the Iceberg's Waterline
Before we start talking about the extensible API design, we should discuss the hygienic minimum. A huge number of problems would have never happened if API vendors had paid more attention to marking their area of responsibility.
##### Provide a minimal amount of functionality
At any moment in its lifetime, your API is like an iceberg: it comprises an observable (e.g. documented) part and a hidden one, undocumented. If the API is designed properly, these two parts correspond to each other just like the above-water and under-water parts of a real iceberg do, i.e. one to ten. Why so? Because of two obvious reasons.
* Computers exist to make complicated things easy, not vice versa. The code developers write upon your API must describe a complicated problem's solution in neat and straightforward sentences. If developers have to write more code than the API itself comprises, then there is something rotten here. Probably, this API simply isn't needed at all.
* Revoking the API functionality causes losses. If you've promised to provide some functionality, you will have to do so ‘forever’ (until this API version's maintenance period is over). Pronouncing some functionality deprecated is a tricky thing, potentially alienating your customers.
Rule \#1 is the simplest: if some functionality might be withheld — then never expose it. It might be reformulated like: every entity, every field, every public API method is a *product solution*. There must be solid *product* reasons why some functionality is exposed.
##### Avoid gray zones and ambiguities
Your obligations to maintain some functionality must be stated as clearly as possible. Especially regarding those environments and platforms where no native capability to restrict access to undocumented functionality exists. Unfortunately, developers tend to consider some private features they found to be eligible for use, thus presuming the API vendor shall maintain them intact. Policy on such ‘findings’ must be articulated explicitly. At the very least, in case of such non-authorized usage of undocumented functionality, you might refer to the docs, and be in your own rights in the eyes of the community.
However, API developers often legitimize such gray zones themselves, for example, by:
* returning undocumented fields in endpoints' responses;
* using private functionality in code examples — in the docs, responding to support messages, in conference talks, etc.
One cannot make a partial commitment. Either you guarantee this code will always work or do not slip a slightest note such functionality exists.
##### Codify implicit agreements
The third principle is much less obvious. Pay close attention to the code which you're suggesting developers to develop: are there any conventions that you consider evident, but never wrote them down?
**Example \#1**. Let's take a look at this order processing SDK example:
```
// Creates an order
let order = api.createOrder();
// Returns the order status
let status = api.getStatus(order.id);
```
Let's imagine that you're struggling with scaling your service, and at some point moved to the asynchronous replication of the database. This would lead to the situation when querying for the order status right after order creating might return `404` if an asynchronous replica hasn't got the update yet. In fact, thus we abandon strict [consistency policy](https://en.wikipedia.org/wiki/Consistency_model) in a favor of an eventual one.
What would be the result? The code above will stop working. A developer creates an order, tries to get its status — but gets the error. It's very hard to predict what approach developers would implement to tackle this error. Probably, none at all.
You may say something like, ‘But we've never promised the strict consistency in the first place’ — and that is obviously not true. You may say that if, and only if, you have really described the eventual consistency in the `createOrder` docs, and all your SDK examples look like:
```
let order = api.createOrder();
let status;
while (true) {
try {
status = api.getStatus(order.id);
} catch (e) {
if (e.httpStatusCode != 404 || timeoutExceeded()) {
break;
}
}
}
if (status) {
}
```
We presume we may skip the explanations why such code must never be written under any circumstances. If you're really providing a non-strictly consistent API, then either `createOrder` operation must be asynchronous and return the result when all replicas are synchronized, or the retry policy must be hidden inside `getStatus` operation implementation.
If you failed to describe the eventual consistency in the first place, then you simply can't make these changes in the API. You will effectively break backwards compatibility, which will lead to huge problems with your customers' apps, intensified by the fact they can't be simply reproduced.
**Example \#2**. Take a look at the following code:
```
let resolve;
let promise = new Promise(
function (innerResolve) {
resolve = innerResolve;
}
);
resolve();
```
This code presumes that the callback function passed to `new Promise` will be executed synchronously, and the `resolve` variable will be initialized before the `resolve()` function is called. But this assumption is based on nothing: there are no clues indicating that `new Promise` constructor executes the callback function synchronously.
Of course, the developers of the language standard can afford such tricks; but you as an API developer cannot. You must at least document this behavior and make the signatures point to it; actually, good advice is to avoid such conventions, since they are simply unobvious while reading the code. And of course, under no circumstances you can actually change this behavior to an asynchronous one.
**Example \#3**. Imagine you're providing animations API, which includes two independent functions:
```
// Animates object's width,
// beginning with first value, ending with second
// in a specified time period
object.animateWidth('100px', '500px', '1s');
// Observes object's width changes
object.observe('widthchange', observerFunction);
```
A question arises: how frequently and at what time fractions the `observerFunction` will be called? Let's assume in the first SDK version we emulated step-by-step animation at 10 frames per second: then `observerFunction` will be called 10 times, getting values '140px', '180px', etc., up to '500px'. But then in a new API version, we switched to implementing both functions atop of a system's native functionality — and so you simply don't know, when and how frequently the `observerFunction` will be called.
Just changing call frequency might result in making some code dysfunctional — for example, if the callback function makes some complex calculations, and no throttling is implemented since the developer just relied on your SDK's built-in throttling. And if the `observerFunction` ceases to be called when exactly '500px' is reached because of some system algorithms specifics, some code will be broken beyond any doubt.
In this example, you should document the concrete contract (how often the observer function is called) and stick to it even if the underlying technology is changed.
**Example \#4**. Imagine that customer orders are passing through a specific pipeline:
```
GET /v1/orders/{id}/events/history
{
"event_history": [
{
"iso_datetime": "2020-12-29T00:35:00+03:00",
"new_status": "created"
},
{
"iso_datetime": "2020-12-29T00:35:10+03:00",
"new_status": "payment_approved"
},
{
"iso_datetime": "2020-12-29T00:35:20+03:00",
"new_status": "preparing_started"
},
{
"iso_datetime": "2020-12-29T00:35:30+03:00",
"new_status": "ready"
}
]
}
```
Suppose at some moment we decided to allow trustworthy clients to get their coffee in advance before the payment is confirmed. So an order will jump straight to "preparing_started", or event "ready", without a "payment_approved" event being emitted. It might appear to you that this modification *is* backwards compatible since you've never really promised any specific event order being maintained, but it is not.
Let's assume that a developer (probably, your company's business partner) wrote some code implementing some valuable business procedure, for example, gathering income and expenses analytics. It's quite logical to expect this code operates a state machine, which switches from one state to another depending on getting (or getting not) specific events. This analytical code will be broken if the event order changes. In the best-case scenario, a developer will get some exceptions and have to cope with the error's cause; worst-case, partners will operate wrong statistics for an indefinite period of time until they find a mistake.
A proper decision would be, in first, documenting the event order and allowed states; in second, continuing generating "payment_approved" event before "preparing_started" (since you're making a decision to prepare that order, so you're in fact approving the payment) and add extended payment information.
This example leads us to the last rule.
##### Product logic must be backwards compatible as well
State transition graph, event order, possible causes of status changes — such critical things must be documented. Not every piece of business logic might be defined in a form of a programmatical contract; some cannot be represented at all.
Imagine that one day you start to take phone calls. A client may contact the call center to cancel an order. You might even make this functionality *technically* backwards compatible, introducing new fields to the ‘order’ entity. But the end-user might simply *know* the number, and call it even if the app wasn't suggesting anything like that. Partner's business analytical code might be broken likewise, or start displaying weather on Mars since it was written knowing nothing about the possibility of canceling orders somehow in circumvention of the partner's systems.
### On the Iceberg's Waterline
Before we start talking about the extensible API design, we should discuss the hygienic minimum. A huge number of problems would have never happened if API vendors had paid more attention to marking their area of responsibility.
##### Provide a minimal amount of functionality
At any moment in its lifetime, your API is like an iceberg: it comprises an observable (e.g. documented) part and a hidden one, undocumented. If the API is designed properly, these two parts correspond to each other just like the above-water and under-water parts of a real iceberg do, i.e. one to ten. Why so? Because of two obvious reasons.
* Computers exist to make complicated things easy, not vice versa. The code developers write upon your API must describe a complicated problem's solution in neat and straightforward sentences. If developers have to write more code than the API itself comprises, then there is something rotten here. Probably, this API simply isn't needed at all.
* Revoking the API functionality causes losses. If you've promised to provide some functionality, you will have to do so ‘forever’ (until this API version's maintenance period is over). Pronouncing some functionality deprecated is a tricky thing, potentially alienating your customers.
Rule \#1 is the simplest: if some functionality might be withheld — then never expose it. It might be reformulated like: every entity, every field, every public API method is a *product solution*. There must be solid *product* reasons why some functionality is exposed.
##### Avoid gray zones and ambiguities
Your obligations to maintain some functionality must be stated as clearly as possible. Especially regarding those environments and platforms where no native capability to restrict access to undocumented functionality exists. Unfortunately, developers tend to consider some private features they found to be eligible for use, thus presuming the API vendor shall maintain them intact. Policy on such ‘findings’ must be articulated explicitly. At the very least, in case of such non-authorized usage of undocumented functionality, you might refer to the docs, and be in your own rights in the eyes of the community.
However, API developers often legitimize such gray zones themselves, for example, by:
* returning undocumented fields in endpoints' responses;
* using private functionality in code examples — in the docs, responding to support messages, in conference talks, etc.
One cannot make a partial commitment. Either you guarantee this code will always work or do not slip a slightest note such functionality exists.
##### Codify implicit agreements
The third principle is much less obvious. Pay close attention to the code which you're suggesting developers to develop: are there any conventions that you consider evident, but never wrote them down?
**Example \#1**. Let's take a look at this order processing SDK example:
```
// Creates an order
let order = api.createOrder();
// Returns the order status
let status = api.getStatus(order.id);
```
Let's imagine that you're struggling with scaling your service, and at some point moved to the asynchronous replication of the database. This would lead to the situation when querying for the order status right after order creating might return `404` if an asynchronous replica hasn't got the update yet. In fact, thus we abandon strict [consistency policy](https://en.wikipedia.org/wiki/Consistency_model) in a favor of an eventual one.
What would be the result? The code above will stop working. A developer creates an order, tries to get its status — but gets the error. It's very hard to predict what approach developers would implement to tackle this error. Probably, none at all.
You may say something like, ‘But we've never promised the strict consistency in the first place’ — and that is obviously not true. You may say that if, and only if, you have really described the eventual consistency in the `createOrder` docs, and all your SDK examples look like:
```
let order = api.createOrder();
let status;
while (true) {
try {
status = api.getStatus(order.id);
} catch (e) {
if (e.httpStatusCode != 404 || timeoutExceeded()) {
break;
}
}
}
if (status) {
}
```
We presume we may skip the explanations why such code must never be written under any circumstances. If you're really providing a non-strictly consistent API, then either `createOrder` operation must be asynchronous and return the result when all replicas are synchronized, or the retry policy must be hidden inside `getStatus` operation implementation.
If you failed to describe the eventual consistency in the first place, then you simply can't make these changes in the API. You will effectively break backwards compatibility, which will lead to huge problems with your customers' apps, intensified by the fact they can't be simply reproduced.
**Example \#2**. Take a look at the following code:
```
let resolve;
let promise = new Promise(
function (innerResolve) {
resolve = innerResolve;
}
);
resolve();
```
This code presumes that the callback function passed to `new Promise` will be executed synchronously, and the `resolve` variable will be initialized before the `resolve()` function is called. But this assumption is based on nothing: there are no clues indicating that `new Promise` constructor executes the callback function synchronously.
Of course, the developers of the language standard can afford such tricks; but you as an API developer cannot. You must at least document this behavior and make the signatures point to it; actually, good advice is to avoid such conventions, since they are simply unobvious while reading the code. And of course, under no circumstances you can actually change this behavior to an asynchronous one.
**Example \#3**. Imagine you're providing animations API, which includes two independent functions:
```
// Animates object's width,
// beginning with first value, ending with second
// in a specified time period
object.animateWidth('100px', '500px', '1s');
// Observes object's width changes
object.observe('widthchange', observerFunction);
```
A question arises: how frequently and at what time fractions the `observerFunction` will be called? Let's assume in the first SDK version we emulated step-by-step animation at 10 frames per second: then `observerFunction` will be called 10 times, getting values '140px', '180px', etc., up to '500px'. But then in a new API version, we switched to implementing both functions atop of a system's native functionality — and so you simply don't know, when and how frequently the `observerFunction` will be called.
Just changing call frequency might result in making some code dysfunctional — for example, if the callback function makes some complex calculations, and no throttling is implemented since the developer just relied on your SDK's built-in throttling. And if the `observerFunction` ceases to be called when exactly '500px' is reached because of some system algorithms specifics, some code will be broken beyond any doubt.
In this example, you should document the concrete contract (how often the observer function is called) and stick to it even if the underlying technology is changed.
**Example \#4**. Imagine that customer orders are passing through a specific pipeline:
```
GET /v1/orders/{id}/events/history
{
"event_history": [
{
"iso_datetime": "2020-12-29T00:35:00+03:00",
"new_status": "created"
},
{
"iso_datetime": "2020-12-29T00:35:10+03:00",
"new_status": "payment_approved"
},
{
"iso_datetime": "2020-12-29T00:35:20+03:00",
"new_status": "preparing_started"
},
{
"iso_datetime": "2020-12-29T00:35:30+03:00",
"new_status": "ready"
}
]
}
```
Suppose at some moment we decided to allow trustworthy clients to get their coffee in advance before the payment is confirmed. So an order will jump straight to "preparing_started", or event "ready", without a "payment_approved" event being emitted. It might appear to you that this modification *is* backwards compatible since you've never really promised any specific event order being maintained, but it is not.
Let's assume that a developer (probably, your company's business partner) wrote some code implementing some valuable business procedure, for example, gathering income and expenses analytics. It's quite logical to expect this code operates a state machine, which switches from one state to another depending on getting (or getting not) specific events. This analytical code will be broken if the event order changes. In the best-case scenario, a developer will get some exceptions and have to cope with the error's cause; worst-case, partners will operate wrong statistics for an indefinite period of time until they find a mistake.
A proper decision would be, in first, documenting the event order and allowed states; in second, continuing generating "payment_approved" event before "preparing_started" (since you're making a decision to prepare that order, so you're in fact approving the payment) and add extended payment information.
This example leads us to the last rule.
##### Product logic must be backwards compatible as well
State transition graph, event order, possible causes of status changes — such critical things must be documented. Not every piece of business logic might be defined in a form of a programmatical contract; some cannot be represented at all.
Imagine that one day you start to take phone calls. A client may contact the call center to cancel an order. You might even make this functionality *technically* backwards compatible, introducing new fields to the ‘order’ entity. But the end-user might simply *know* the number, and call it even if the app wasn't suggesting anything like that. Partner's business analytical code might be broken likewise, or start displaying weather on Mars since it was written knowing nothing about the possibility of canceling orders somehow in circumvention of the partner's systems.
*Technically* correct decision would be adding ‘canceling via call center allowed’ parameter to the order creation function. Conversely, call center operators may only cancel those orders which were created with this flag set. But that would be a bad decision from a *product* point of view. The only ‘good’ decision in this situation is to foresee the possibility of external order cancels in the first place. If you haven't foreseen it, your only option is the ‘Serenity Notepad’ to be discussed in the last chapter of this Section.