9
Evolving an API design

This chapter covers

  • Designing evolutions to avoid breaking changes
  • Versioning APIs to manage breaking changes
  • Designing extensible APIs to limit breaking changes

In the previous chapters, you learned how to design APIs that provide features or goals that make sense for their users. You also learned how to design user-friendly and secure representations of these goals. Once all that work is done, is that the end of the API designer’s job? Not at all! It’s a new beginning.

An API is a living thing that will inevitably evolve, perhaps to provide new features or enhancements to existing ones. To design for such evolutions, you can reuse the same skills you have learned up to this point—but designing evolutions requires some extra care.

Over the years, I’ve bought several Ikea Billy bookcases to store books, comic books, CDs, LPs, and many other things. A standard Billy bookcase comes with four movable shelves that you can place as you want, thanks to the many holes drilled on both of the internal sides. You just have to place four pegs at the desired height, place the shelf on them, and you’re done. If a bookcase is used to store small items like CDs or paperback books, using only four movable shelves can leave a lot of empty space. Fortunately, additional shelves can be bought independently, allowing you to use the empty space to store more small items. But the last time I bought extra shelves for a Billy I’d owned for quite a long time, I had an unpleasant surprise—the pegs were too small to fit well in the holes of my good old Billy.

The 2.0 Billy system uses pegs with a smaller diameter, which are incompatible with the holes of the previous version. I wasn’t aware of this until I tried to use these new pegs; it wasn’t indicated anywhere when I bought my extra shelves. This was a real bummer. Such breaking changes can happen when evolving an API too.

What might happen if the Banking API’s provider decided that the balance’s currency must now be returned with the bank account’s information in order to support various currencies? Figure 9.1 shows how evolving the Banking API in this way could lead to a scenario equivalent to the Billy’s peg diameter modification.

09-01.png

Figure 9.1 A consumer encountering a breaking change after updating the Banking API

In this scenario, the balance property, which was a number, is now an object containing a value (the former balance) and its currency (an ISO 4217 currency code string). Although using an ISO 4217 currency code is clever (as you learned in section 6.1.4), modifying the balance this way is definitely not a good idea. Indeed, the Awesome Banking App crashes when parsing the returned data because it expects the balance to be a number, not an object. The developers in charge of coding this consumer would usually catch such errors, avoiding crashes like this, but the errors will still prevent the consumer application from functioning properly. To fix this problem, the mobile application needs to be updated.

A breaking change is a change that will cause problems for consumers if they do not update their code. Most of the time you cannot synchronize an API update with all of its consumers' updates, so trying to avoid such breaking changes or at least being aware of them when designing an API’s evolutions is definitely important. We can carefully design our evolutions in order to avoid introducing some problematic changes. We can even design APIs from the ground up in order to prevent them. But regardless of how carefully we design our APIs and their evolutions, sooner or later, a breaking change is inevitable.

As API designers, we must also be aware of the invisible side of the API contract—all observable behaviors that are not explicitly described in the interface contract that can silently evolve and provoke totally unexpected breaking changes.

To deal with these situations when they come up, it is wise to know how to version our APIs. We’ll explore all of these topics in this chapter. We’ll start by learning how to (carefully) design evolutions.

9.1 Designing API evolutions

The Banking API evolution described in this chapter’s introduction illustrated a possible way of introducing a breaking change. The consumer crashed because of a modified data structure that became impossible to parse (a number became an object). But that’s not the only way of introducing a breaking change. Although some are pretty obvious, like this data structure modification, others are more insidious, like modifying the possible values of a property. Also, the consequences might not always be that obvious because the changes might not cause a visible error on the consumer’s side. A breaking change could even have impacts on the provider’s side. I’ll let you imagine what the consequences could be for both consumers and the provider if amount values in dollars were replaced by values in cents in the Banking API, especially for the transfer money goal.

Any modification of the interface contract of an API that can be described formally using an API description format or via textual API documentation can introduce a breaking change. This applies to output data, input data, parameters, response statuses or errors, goals and flows, and security. Knowing how to avoid breaking changes when possible and handling them gracefully when not is, therefore, critical for an API designer.

9.1.1 Avoiding breaking changes in output data

The Banking API proposes a list transactions goal that returns a list of transactions for an account number. The left part of figure 9.2 shows the data returned for each transaction. The right part shows a redesigned version that illustrates various ways of introducing breaking changes that will cause problems to consumers when retrieving an account’s transactions list.

09-02.png

Figure 9.2 How to introduce breaking changes in the list transactions goal’s output

The new designer has renamed the amt property amount, to make its meaning clearer. Based on what you have learned, this is good design, but this change can have a significant impact on the consumer side. The Android version of the Awesome Banking App, which is not as well coded, can crash with the famous java.lang.NullPointerException. The iOS version might just show the transactions without amounts, leaving its end users quite annoyed. A more complex Financial Statistics consumer that does some calculations based on the transaction amounts might consider that each transaction’s amount is 0, and this could corrupt its data. Also, moving the merchantName and merchantZip properties into a new merchant structure (because a property for the merchant city has been added, and the designer has presumably read section 7.1.1 of this book) are also examples of breaking changes causing a parsing exception.

What about the aboveAverageAmount property that was removed, perhaps because this information was not considered important? Is this a problem too? Definitely, because this property is mandatory. In the initial version of this transaction, it was supposed to always be provided, so removing it could cause the same sorts of problems as renaming amt or moving merchantName.

Another issue is with the transaction’s type, which was a number, indicating if it was a card (1), transfer (2), or check transaction (3). It is now a string (because the new designer knows that human-readable codes are usually better than cryptic ones). Although benevolent, such a change will probably cause a parsing error on the consumer side; and even if it doesn’t, interpretation of the new values will likely be impossible.

Renaming, moving, or removing properties and changing their types are obvious ways of introducing breaking changes in output data. But some other modifications are more insidious.

The category property was mandatory, but it has been made optional. Consumers used to always get it, and now, if they don’t, they could face the same problems described in the amt renaming case. The date property was a string, and it still is, but its format has changed. It was a UNIX timestamp with a string format. While it was usually represented by a number, now it’s an ISO 8601 date (the new designer decided to fix this awkward design). Again, this change is benevolent, but it will cause parsing errors on the consumer side.

The modification to the label property can also be a breaking change. Its maximum length has been changed from 25 to 150, perhaps because the core banking system behind the Banking API has been updated in order to manage full labels and to stop truncating these. If, on the consumer side, this value is stored in a good old relational database, where its column’s size is defined as 25, it will be impossible to store the longer values. These are insidious breaking changes, but there are even less obvious ones.

Look more closely at the descriptions. In the original version, the amt description states that the transaction’s amount is in cents; but, in the new one, the value is in dollars. When receiving an amt value such as 3034 (cents), consumers understood it to be $30.34. Now, they will receive an amount value of 30.34 in dollars and will understand it as $0.3034. This might provoke some panic on the consumer side.

Less critically, the categorizationStatus was a numerical code indicating how the transaction has been categorized: 1 for automatic and 2 for manual. In the new version, a new code value has been added. The consumers will not be able to interpret the value 3 without being updated. And even if this code had been a human-readable one, such a modification might have been a problem because the application consuming the API might not have been able to interpret it.

That’s a lot of different ways of introducing breaking changes. Table 9.1 sums up the various types of modifications and their consequences.

Table 9.1 Breaking changes to output data and their consequences

Modification Consequences
Renaming a property Varies, depending on implementation (missing data in UI, data corruption, crash, and so forth)
Moving a property Varies, depending on implementation (missing data in UI, data corruption, crash, and so forth)
Removing a mandatory property Varies, depending on implementation (missing data in UI, data corruption, crash, and so forth
Making a mandatory property optional Varies, depending on implementation (missing data in UI, data corruption, crash, and so forth)
Modifying a property’s type Parsing error
Modifying a property’s format Parsing error
Modifying a property’s characteristics (increasing string length, number range, or array items count) Varies, depending on implementation (database errors, and so forth)
Modifying a property’s meaning Expect the worst
Adding values to enums Varies, depending on implementation (missing data in UI, data corruption, crash, and so forth)

This list might not be 100% complete, but you get the idea. As you can see, modifying existing elements in the output data can cause more or less obvious breaking changes with more or less significant consequences.

Now that we know how to introduce breaking changes in output data, let’s see what kinds of modifications can be done safely. Figure 9.3 shows a backward-compatible evolution of the transaction’s schema.

09-03.png

Figure 9.3 Designing backward-compatible modifications to the output data

The merchant city information has simply been added as merchantCity without modifying the existing merchant properties. The new categorization status, which was supposed to indicate that the categorization was automatic, although based on other customers' data, is handled with the new communityCategorization Boolean flag. And replacing the transaction type code numbers with human-readable ones is handled by adding a new typeLabel property. Consumers will not be bothered by these new elements.

Another change is to the type property, which was optional and now has become mandatory. Instead of sometimes getting this property, consumers will always get it. Unlike making a mandatory property optional, this is a nonbreaking change. Is it necessary to make such a change? Probably not. It’s not really critical. Consumers can go on using the API without being notified of the transaction type.

The label’s format has also been changed in a backward-compatible way (purely for the purposes of illustration): its maximum length is now 25 instead of 100. As another example of a nonbreaking change, the optional categorizationStatus property could be removed. Depending on how the data is serialized (not all APIs use JSON), that could cause some problems, so it would be better to keep it and always return a null value instead.

Notice that some of the modifications that were supposed to fix bad design, like changing the amt property’s name, couldn’t be made. This is something that API designers have to live with. Once consumers start to use a poorly designed API, it is impossible to fix it completely most of the time without introducing breaking changes.

The final result might not be the best design, but at least it lets the new designer introduce new features and partially fix some early design mistakes without breaking consumers’ code. To be frank, the safest way to modify output data is purely and simply to add new elements. For new features (like merchantCity), that’s quite simple: just add the new required data. But when it comes to slightly modifying existing ones (like the categorization status value), it is trickier to find a solution. There’s no magic recipe, but you can try two approaches, as shown in figure 9.3.

First, you can treat this new value as a flag, as was done in figure 9.3 by adding the communityCategorization Boolean. Second, you can add a new property (say, extendedCategoryStatus) that shows the same data as categorizationStatus plus the new status. If this were done multiple times, however, the resulting design, comprising duplication of data, could be awkward and make the API difficult to understand.

Now, what about when modifying input data and parameters? Does it work the same way? Almost. And this subtle difference is important to know. Let’s introduce some breaking changes in the transfer money goal’s input data to contrast this with what you’ve learned about output data modifications.

9.1.2 Avoiding breaking changes to input data and parameters

The input shown on the left in figure 9.4 has been slightly modified from what we worked with in previous chapters to illustrate different (possible) breaking changes. The right part of the figure shows various ways a new designer evolving the API might introduce breaking changes in the input data.

09-04.png

Figure 9.4 How to introduce breaking changes in the transfer money goal’s input data

If the designer renames amt to amount, a non-updated consumer sending a transfer money request using amt will get an error. Depending on how the implementation is done, this error could be because amt is now an unexpected property or because the new amount property is mandatory. For a REST API that would mean that it would return a 400 Bad Request response status.

The same goes for moving a property (here, moving destination inside target), modifying a property’s type (changing source from a number to a string), and modifying formats in general (changing the format of date and the range of amount).

As with the output use case, modifying a property’s meaning is a silent breaking change. The amount was previously in cents and is now in dollars; therefore, a consumer sending a money transfer of 8,000 cents will instead trigger a transfer of 8,000 dollars. That’s a terrible side effect, and it will be up to the bank providing the API to get the money back because the error is on its side.

These breaking changes have a similar effect in both the input and output data use cases, but this is not the case for removing a mandatory property, making a mandatory property optional, or adding values to enums. Removing the mandatory type property is a breaking change that will cause an unexpected property error. It would be exactly the same if type were optional. Making a mandatory property optional has absolutely no consequences for the provider or consumers, but making an optional one (such as currency) mandatory will cause missing mandatory property errors. Adding values to enums also has no consequences, but removing values will cause invalid value errors.

So does this mean that, like in the output data case, adding data is the safest way of modifying input data? Not quite. Adding a mandatory property has the same consequences as turning an optional one into a mandatory one: the API will return a missing mandatory property error.

Breaking changes are slightly different when it comes to inputs and outputs. Table 9.2 sums up the possible types of breaking changes to input data and their effects, and compares them to the effects of the same or similar changes to output data.

Table 9.2 Breaking changes to input data and their consequences

Modification Consequences Effects on input vs. output
Renaming a property API error Identical
Moving a property API error Identical
Removing a mandatory or optional property API error Identical
Making an optional property mandatory API error Opposite (same as making a mandatory property optional)
Modifying a property’s type API error Identical
Modifying a property’s format API error Identical
Modifying a property’s characteristics (decreasing string length, number range, or array items count) API error Opposite (same as increasing)
Modifying a property’s meaning Expect the worst (impacts mostly provider) Opposite (impacts mostly consumer)
Removing values from enums API error Opposite (same as adding values)
Adding a mandatory property API error No error (not a breaking change)

So how can we modify input data in a backward-compatible way? Let’s analyze figure 9.5 to find out.

09-05.png

Figure 9.5 Designing backward-compatible modifications to the input data

You might have already guessed that the safest way is to only add optional properties. The destinationBank property is optional, so if consumers don’t provide it in their requests, it will not cause an error. But note that the name property inside destinationBank is mandatory. Indeed, if the added properties are objects, it doesn’t matter if their properties are mandatory or optional.

We can also do two other types of modifications safely: a preexisting mandatory property like type can be turned into an optional one, and we can slightly modify a property’s characteristics. For example, the range of amt (in cents) could be modified from 0 to 1000000 to a broader 0 to 1500000. It is also possible to increase a requested string’s maximum length or the number of items in an array. Note that for REST APIs, all of this also applies to query parameters and HTTP request headers.

We have seen that carelessly modifying a goal’s input parameters or data can cause errors. These can be subject to breaking changes too. From a broader perspective, modifying how an API provides feedback, whether on success or error, is prone to introduce breaking changes.

9.1.3 Avoiding breaking changes in success and error feedback

Depending on the protocol used, feedback about how the processing of a request went can vary, but it is usually based on a combination of the data returned as a response to a request and some of the protocol’s features. We’ll talk about the data first.

For both success and error feedback, the data returned can be safely modified based on what you learned in section 9.1.1. There, you saw how to modify success feedback, so let’s try to modify an existing error response.

As shown in figure 9.6, we could change the error feedback of the transfer money goal in order to introduce some breaking changes. In the modified version, the items property has been renamed errors. Consumers will not be able to get the detailed error information required to fix a problem now because they expect to find it in items. The same goes for the type property’s values MISSING_SOURCE and MISSING_DESTINATION, which have been replaced by a generic MISSING_MANDATORY. The consumers will not be able to interpret the new error type.

From a pure data perspective, this means that error and success feedback data should be treated equally when it comes to modifying them in order to avoid breaking changes. From a functional perspective, the second breaking change means that we cannot introduce new types of errors into existing goals or change existing ones.

09-06.png

Figure 9.6 Introducing breaking changes in error feedback

This example shows us an interesting thing about breaking changes, and, more specifically, about the scope of their consequences. Modifying the type value has an impact that is local to the goal itself, but renaming items to errors has a more global impact. Indeed, because the error message data structure is probably the same across the whole API, this renaming might have been done not only for the transfer money goal but for all goals. That means that not a single consumer will be able to interpret the error feedback of any goal without updating its code. That’s quite a breaking change.

Breaking changes with such wide impact are not limited to errors, but can happen when modifying any common feature of an API. For example, changing naming conventions for resource IDs to comply with better guidelines is a global breaking change affecting inputs and outputs. So it’s better to look twice at such modifications and apply what you learn in this chapter about avoiding breaking changes. And regarding protocol features, let’s see what could happen if we modify HTTP status codes (figure 9.7).

09-07.png

Figure 9.7 Modifying HTTP status codes

The RFC 7231, which describes the HTTP 1.1 protocol, states

“HTTP clients are not required to understand the meaning of all registered status codes, though such understanding is obviously desirable. However, a client MUST understand the class of any status code, as indicated by the first digit, and treat an unrecognized status code as being equivalent to the x00 status code of that class….”

RFC 7231

That means that adding new status codes should not be a problem at all. Indeed, a HTTP client must treat an unrecognized status code as being equivalent to the x00 status code of that class. So when using the transfer money goal, a consumer receiving an unknown (until now) 201 Created must treat it like they would treat a 200 OK. The same goes when receiving a new 429 Too many requests; consumers should treat it as a basic 400 Bad Request.

It also means that returning a new class like 5XX on errors should not be a problem at all. Indeed, “a client MUST understand the class of any status code.” Therefore, even if the transfer money goal was not known to return a 500 Internal Server Error according to its documentation, consumers must be ready to handle it. And replacing status codes with codes of the same class should not cause too many problems either: a 201 Created is a success, just like a 202 Accepted. Indeed, the same class code means the same type of error. And, obviously, replacing a 429 Too many requests by a more generic 400 Bad Request makes the feedback less accurate.

This is all well and good, but that’s true only if we live in an ideal world—and we do not. Some consumers can only be implemented based on what the documentation says and not strictly following RFC 7231. So an unexpected 500 Internal Server Error will likely cause an unexpected consumer error. Some consumers might follow too strictly your functional interface contract. If a delayed money transfer is acknowledged with a 201 Created status, returning a 202 Accepted will likely cause bugs on some of them, even if you provide generic and self-descriptive feedback, because consumers only expect a 201 Created and nothing else. So you have to hope that consumers strictly implement RFC 7231 and do not follow your functional interface contract too rigidly. That is quite tricky if you do not work closely with them.

The only nonbreaking change that could be done safely would be to remove an HTTP status code because the underlying error will never happen due to some modifications in the implementation. All other modifications, even if they are supposed to be accepted according to RFC 7231, should not be done without extreme caution. Never trust consumers if you don’t know how they are actually coded. Of course, this is for the HTTP protocol. If your API relies on another protocol, you will have to check how that protocol works to determine how best to handle feedback modifications based on what you have learned here.

9.1.4 Avoiding breaking changes to goals and flows

Breaking changes can also occur at a higher level when modifying goals and flows. Regarding goals, we already know that modifying inputs, outputs, or feedback is likely to cause breaking changes, but that’s not all.

There are two other obvious ways to introduce breaking changes: by renaming or removing goals. For example, the Banking API proposes the goals transfer money and list transfers. These goals are represented by POST /transfers and GET /transfers, respectively. If we decided to rename the transfer resource as money-transfers, consumers using these two goals would get a 404 Not Found response. You could take advantage of the 301 Moved Permanently HTTP status code to redirect all calls on /transfers to /money-transfers. But that works only if consumers understand and actually follow the redirection, as shown in the next listing.

Listing 9.1 Activating a redirects flag on a Java HttpUrlConnection

URL obj = new URL("https://api.bankingcompany.com/transfers");
HttpURLConnection conn = (HttpURLConnection) obj.openConnection();
conn.setInstanceFollowRedirects(true);
HttpURLConnection.setFollowRedirects(true);  

①   No redirection will be made without setting a flag explicitly.

Such a configuration might simply be unknown (not all people are HTTP experts), and it can also be deactivated purposely for security reasons. Some consumers might not want to get their request sent to somewhere else without their approval and, more than likely, would prefer to get an error in their code.

Another obvious breaking change would be to remove a goal. Indeed, if we decided to remove the GET method on the transfer resource, consumers using the list transfers goal would get a 405 Method Not Allowed response. Clearly it’s better not to remove or rename goals, but does that mean that we can add goals as we please?

Let’s say that, for security reasons, the Banking API’s designers decide that every money transfer must be validated by the source account owner using a one-time password (OTP) received by SMS. One way to handle this modification would be to add a new validate transfer goal, which must be called after transfer money. It could expect a transfer ID and this OTP, which is sent upon receipt of the transfer money request. The transfer money goal’s interface is not changed at all, but because non-updated consumers will never call the new validate transfer goal, they will not be able to trigger money transfers anymore. Even worse, because there is no error, they won’t be aware of the problem—it’s a silent breaking change.

Adding a new transfer money securely goal would not break anything, but it would not secure anything either because consumers would still be able to call the original nonsecure transfer money goal. Introducing a new mandatory step in the existing flow is a breaking change, as is modifying the behavior of existing goals. All you can do at the goal/flow level is add entirely new goals that consumers do not need to use for existing flows. But when doing that, you must pay attention to security.

9.1.5 Avoiding security breaches and breaking changes

Modifying an API can introduce breaking changes that affect security and open up the risk of security breaches; therefore, all API modifications must be made with security in mind. Basically, you must apply everything you learned in chapter 8 when modifying an API in any way. For example, for any data added to existing goals' responses, you must ensure that this data will not be provided to consumers that are not supposed to get it.

You must also be careful when modifying scopes. Some modifications could lead to security breaches or breaking changes, as shown in figure 9.8.

09-08.png

Figure 9.8 Introducing security breaches and breaking changes when modifying scopes

First, depending on the chosen security partitioning strategy (see section 8.2), introducing a delete account goal represented by a DELETE /accounts/{accountId} request, for example, could be problematic. If the partitioning is resource-based, any consumer with access to the /accounts resource would get access to this quite sensible but dangerous goal.

Second, if adding a new goal to an existing scope is subject to caution, what about removing one? Let’s say the access account information scope comprises the list accounts, read account, list transactions, and list transfers goals. Based on what you learned in section 8.2, we know that such a security partitioning, covering different topics, is quite awkward. Maybe we should remove the list transfers goal from this scope to make it more understandable. But that would mean existing consumers with the access account information scope will not be able to use it anymore. So removing a goal from a scope introduces a breaking change. And finally, renaming or removing a scope will have the same effect: consumers will lose access to all goals that were covered by it.

It’s not usually up to the API designers to decide how an API is actually secured, but you should know that modifying security aspects of an API can introduce breaking changes. Before reading what follows, think about what you learned in chapter 8 and try to figure out other ways security modifications could introduce breaking changes or security breaches. I’ll be back in a minute. When you are done, you can read what follows.

Based on what you have learned so far, you should understand that changing how tokens are acquired (replacing an OAuth 2.0 flow by another, for example) will irremediably cause breaking changes on the consumer’s side. Modifying how they should be passed in a request (changing data) is also a breaking change. And last, but not least, as the application/system handling identification can be independent from the API, it could be modified without the knowledge of the people in charge of the API’s implementation. Modifying the security data attached to tokens in this way could have terrible consequences on the implementation.

For example, removing the end user ID from the data attached to the access token will, at best, cause unexpected server errors and, at worst, security breaches; the implementation thinking that, as there is no end user involved, the consumer is of an admin type. It is always good to be aware of this, and to remind other people working on the API about the impacts of such modifications.

9.1.6 Being aware of the invisible interface contract

So far, what you have seen concerns the visible part of the interface contract: everything that can be described using an API description format or documentation. But some consumers might also rely on the invisible parts of the API’s interface contract.

For example, an account owner might have different addresses. These addresses are returned in a list, and each one has a type property indicating if it is a home, office, or temporary address. Let’s say some shrewd developers have spotted that the addresses are always ordered home, then office, then temporary. So when they want to get the home address, they use its index (0) instead of scanning the list seeking an address with a type of home. We all agree that this is total nonsense; consumers must not do that. But if they do, and if this order changes, these consumers will break in a silent way and show the wrong address.

Another example of this invisible interface contract is that consumers might decide that a transaction label, which is just described as a string without any other details, cannot be longer than 50 characters based on the data they have retrieved so far. We already know what can happen if the length of these labels is extended: probably some database errors. As you can see, consumers can rely on parts of the API that are not explicitly described. Indeed, Hyrum’s law states that

“With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody.”

Hyrum's law (Hyrum Wright)

What could happen if the transfer money goal was modified from a purely internal perspective (without introducing any modification in the visible interface contract) to add new controls that slightly extend the goal’s response time? Some consumers that have tuned their timeouts according to the actual response time can break because the new version of the goal takes longer than their timeout value. These considerations might not be obvious, but any API designer (or anyone working on APIs) must be aware of the invisible parts of the interface contract in order to properly evaluate the importance of any change made to an API.

We have covered many different ways of introducing the dreaded breaking changes. But should we always be afraid of them?

9.1.7 Introducing a breaking change is not always a problem

A breaking change is a change that will cause problems for consumers if they do not update their code. As you’ve seen, these problems can also have repercussions on the provider’s side. If the Banking API’s consumers are third-party applications developed by other companies, introducing breaking changes to the API is definitely not an option. The consumers will break, and their developers will get upset, lose confidence in the Banking API, and possibly choose to use a competing API instead. Therefore, the Banking Company can lose money—if not worse.

But not all APIs are public ones, consumed by thousands of third-party consumers. If the Banking API were a private one, acting as a simple backend for a single-page application (SPA) as well as a mobile application built by the Banking Company itself, introducing breaking changes would be practicable. All that is required in this case is to update the SPA on the Banking Company’s web server hosting the SPA files and force an update of the mobile application, provided that this application includes a force update feature.

As you can see, depending on the context, introducing a breaking change might not be a problem as long as all consumers can be updated synchronously with the API. But to be frank, doing so might not be an easy task. The most secure option if a breaking change is inevitable is to version the API.

9.2 Versioning an API

The day has arrived! The Banking Company has decided to launch version 2 of its famous Banking API. Figure 9.9 shows a possible scenario, among many others, to handle such a change.

09-09.png

Figure 9.9 The Banking Company has updated its Banking API to version 2.

The new version of the Banking API is available at apiv2.bankingcompany.com. Consumers switching to this new version get awesome new features, such as the ability to make international money transfers in any currency, thanks to the brand-new 3.0.0 banking engine written in Go that has replaced the good old 1.2.0 COBOL one. But, unfortunately, it is not fully backward-compatible.

When switching to this new version, consumers will also need to update their code because some goals, such as the list transactions goal, have been modified in a non-backward-compatible way to make them consistent with the new features. The Banking Company has also announced that it will support version 1 (exposed at api.bankingcompany.com) for 12 months. This means consumers have 12 months to upgrade, even if they do not use the modified features or intend to use the new ones. It also means that the Banking Company will have to run two versions of its backend for 12 months.

In this case, from the API designer’s perspective, the versioning work lies in the non-backward-compatible design—introducing breaking changes and changing the domain name to differentiate the two versions of the API. But API versioning is a subject that goes beyond just API design, and API designers, like any other person working on an API, must be aware of all of its implications.

Besides design, API versioning has impacts on implementation and product management. Indeed, choosing a versioning strategy affects not only how you design an API but also how you implement it (the Banking Company provides the two versions of the API using two separate backends). Also, just because you provide a new version of your product (your API) doesn’t mean consumers will be willing to switch to it; many of them might prefer to stick with the previous version. Before we explore the various ways of representing API versioning and its impacts, however, let’s clarify what API versioning is and differentiate it from implementation versioning.

9.2.1 Contrasting API and implementation versioning

The initial version of the Banking API only provided access to account information, but it quickly evolved to offer more features. Figure 9.10 shows the evolution of this API and its implementation.

09-10.png

Figure 9.10 The evolution of the Banking API and its implementation

Right after its launch, the API was updated to provide the capability of making money transfers. The implementation obviously had to be updated to provide the new transfer-related goals. After this update, the API and the implementation shared the same 1.1 version number. Unfortunately, the first version of the transfer implementation was not really effective. Each money transfer was processed synchronously on each API call. This resulted in long response times, especially when there were more than 100 transfer requests per second. It was then decided to modify the implementation in order to put money transfer requests in a message queue and process them asynchronously without impacting the API.

The new 1.2 implementation was far more effective, while still exposing the same 1.1 API. After that, the Banking Company’s CTO became fond of Go and decided to get rid of COBOL. The first attempt was to automatically convert the COBOL code into Go code. Although the Banking API used a completely different programming language, version 2.0 of the implementation was able to expose version 1.1, so consumers did not notice this change at all. Unfortunately, before going live in production, the generated code was revealed to be inefficient and poorly written. So a full manual rewrite was done, and long-awaited new features were also added. But the most important change was that the oldest goals of the API, the account-information-related ones, were modified to match the API design rules introduced with the transfer features in version 1.1. This breaking change forced the Banking Company to update the API to a non-backward-compatible 2.0 version.

As you can see, an API has a version, like any software component, but it is not correlated to the version of its implementation. The version of an API evolves based on the changes made to the interface contract (the changes that are visible from the consumers' perspective) and not on how the implementation evolves. Two completely different implementations can expose exactly the same version of an API. In this example, the API and implementation version names, like v1.1 or v3.0.0, are based on a well-known and clever system of using numbers to name a software component version—semantic versioning (https://semver.org/). It consists of using three digits in this format: MAJOR.MINOR.PATCH. Each is incremented in specific situations:

  • The MAJOR digit is incremented only on breaking changes, such as adding a new mandatory parameter (see section 9.1).
  • The MINOR digit is incremented when new features are added in a backward-compatible manner, like adding new HTTP methods or resource paths in a REST API.
  • The PATCH digit is incremented when the modifications made involve backward-compatible bug fixes.

This makes sense for an implementation, but not for an API. Semantic versioning applied to APIs consist of just two digits: BREAKING.NONBREAKING. This two-level versioning is interesting from the provider’s perspective; it helps to keep track of all the different backward-compatible and non-backward-compatible versions of an API. But consumers don’t really care about all those details.

Consumers who were using the account-information-related goals of version 1.0 of the API can seamlessly switch to version 1.1 (NONBREAKING) as if nothing has changed. And even if they decide to use the new transfer features added to v1.1, they’re still simply using “the Banking API” without really caring (or knowing) about its exact version number.

Consumers will only really notice changes in the API when the Banking Company introduces version 2.0. Indeed, they will have to actually modify some parts of their code to use it. From the API consumers' perspective, they simply use version 1 or version 2. They don’t care about the second level of versioning (NONBREAKING); they only need the BREAKING digit.

If removing or renaming a goal leads to a major version bump, it might not be that obvious to do the same thing thing for the invisible modification we discussed in section 9.1.6. It will have to be discussed for each case, and we must evaluate its true impact on consumers in order to determine if releasing a new version is necessary in such cases.

If only a single level of versioning matters for consumers, we can use anything as version names. We could use ISO 8601 dates, such as 2017-10-19 for version 1 and 2018-22-12 for version 2. If we wanted, we could even use famous anime soundtrack composers' names, such as Yoko Kanno and Kenji Kawai for versions 1 and 2, respectively.

API and implementation versioning are different, and consumers (mostly) only care about the version changes announcing breaking changes. But how do consumers actually tell which version of an API they want to use?

9.2.2 Choosing an API versioning representation from the consumer’s perspective

The Banking Company has rolled out its brand-new Banking API v2.0, which is not completely backward-compatible. Hopefully, the transfer-related goals are backward-compatible, so consumers using those in the previous version of the API will only have to tweak their requests a little to switch to this new version. Figure 9.11 shows the different possibilities the Banking Company might choose to actually expose the different versions of the API.

09-11.png

Figure 9.11 Various ways of indicating the version of an API in a request

The Banking API could use the resource’s path to handle the API’s version. Consumers wanting to list transfers might send a GET /v1/transfers or GET /v2/transfers request on the same api.bankingcompany.com domain to use version 1 or 2, respectively, of the API. A similar approach would be to use different domains or subdomains for each version of the API: here, api.bankingcompany.com for version 1 and apiv2.bankingcompany.com for version 2.

The version of the API used can also be indicated via a query parameter (GET /transfers?version=2) or a custom header (Version: 2). Or the Banking API could propose to indicate the version of the API desired using content negotiation, as you learned in section 6.2.1. For this, consumers indicate a custom media type in the standard Content-type header, such as application/vnd.bank.2 to indicate that they want to use version 2 of the API.

And last but not least, the version of the API used can be indirectly indicated in the request. Because the API is secure, consumers have to send some credentials with each request; with this approach, the request contains an Authorization header with a token (here, 4R57TD78). According to the data stored by the provider in the TOKENS table, this token has been generated for the cnsmr_1 consumer (obviously in the real world, nobody would ever store such sensitive data without encryption). The version of the API used by this consumer is indicated in the VERSION column of the CONSUMERS_CONF table.

That’s six different ways of indicating the version for an HTTP-based API. Which one should you choose? Obviously, this choice must be made from the consumer’s perspective.

The simplest options are path and domain versioning. Changing a domain name or path in a URL is quite straightforward, especially if the API is tested with a browser or a curl command line. Consumers can see what version is being used by looking at the URL they use. These are probably the most used options; and, based on what you learned in section 6.1.4, it’s worth taking those into consideration. Your consumers, who are probably already familiar with these mechanisms, will find them easy to use.

Query parameter versioning is also a quite simple option; but, from an API designer’s perspective, I don’t recommend it because it is not really clean. For example, if we add a currency filter as in GET /transfers?currency=eur&version=2, the query mixes a purely technical parameter with a functional one.

Content-type versioning is interesting from an HTTP expert’s perspective, but many people are reluctant to use HTTP headers despite the fact that it is not complicated at all. This problem is exacerbated with the custom HTTP header option because it’s not part of the HTTP standard.

The consumer configuration option is totally consumer-friendly in that there’s no need for consumers to modify their code. One small drawback is that it requires updating the configuration to switch from one version to another, which can be cumbersome when testing different versions of the API.

What would your preferred choice(s) be? Personally, I prefer path and consumer configuration versioning, but let’s step back and look beyond REST, HTTP, and personal preferences.

We can see that there are three ways to expose different versions of an API. The first one is simply to consider the new version as a new API and create a new exposition endpoint. The second one is to keep a single endpoint for the various versions but to pass a parameter in requests, using some protocol features or metadata in the request data, which indicates the version of the API used. The third one also uses a single endpoint, but stores the version used by each consumer on the provider side. Whatever the technical solution adopted, the choice of how to indicate the version of the API used must take into consideration standards and usability in order to ensure that consumers will be able to understand it and use it easily.

So far, we’ve talked about API versioning. But is versioning an entire API the only option?

9.2.3 Choosing API versioning granularity

Versioning an API as a whole is the most common practice, but not the only one. Depending on the use case and type of API, other options can be more effective.

For REST APIs, besides at the API level, versioning can be done at the resource level, the goal/operation level, and the data/message level. Figure 9.12 compares API versioning and resource versioning when breaking changes are introduced. Note that the breaking changes in this example could be avoided based on what you have learned in section 9.1.

09-12.png

Figure 9.12 API versioning versus resource versioning

To keep the example simple, the Banking API is reduced to three goals: transfer money, list transfers, and delete transfer. The left side shows what happens when versioning the API as a whole, and the right side, what happens when versioning each resource identified by its path. On both sides, the version number is located on the first path level. Take a look at the top of this figure. On the left, the API version is v1. On the right, the version of the two transfer resources (/v1/transfers and /v1/transfers/{id}) is v1.

The transfer money goal expects a source and a destination account number, and an amount of money. A first breaking change is introduced by adding a new mandatory currency property to the input of this goal. On the API versioning side, this single breaking change forces us to create a new v2 of the API. If consumers compare the goals of the two versions, they will have absolutely no clue about what has changed without reading the API’s release notes.

On the resource versioning side, no new API is created, but a new /v2/transfers resource is added in order to manage the modification of the POST /v2/transfers operation. That gives consumers a hint, but it is impossible to know which operation on the transfer resource has been modified without reading the release notes.

Two new goals are also introduced, and the transfer money goal is modified in order to facilitate money transfers and manage transfers to external accounts. The list sources goal allows a consumer to list all possible sources for a money transfer, and list destinations gives possible destinations for a selected source. Introducing these two new goals is not a breaking change. But unfortunately, the sources and destinations are each identified by a number, which is different from the string account numbers expected by the transfer money goal. Its input data is modified, thereby introducing a breaking change.

On the resource versioning side, a new v3 transfer resource is added with the /v3/transfers path, along with GET /v1/sources and GET /v1/sources/{id}/destination operations. The API now comprises three different versions of the transfer resource, and the new source and destination resources can only be used with version 3. That’s not easy for consumers to guess.

On the API versioning side, a new v3 API is again created, but there’s no need to think about which versions can be used together. Each independent API version contains a set of compatible resources.

Let’s go now to a deeper level of versioning—at the goal or operation level. Figure 9.13 compares API versioning and goal/operation versioning when the same breaking changes are introduced.

09-13.png

Figure 9.13 API versioning versus goal (or operation) versioning

The API versioning side is exactly the same; but on the other side, each operation is now versioned independently, still using the vX in the path. On each breaking change to the transfer money goal, a new request is added to the API (POST /v2/transfers then POST /v3/transfers). That’s useful because it clearly indicates which goal has been modified. But as with the resource versioning use case, the API ends up with three different versions of the transfer money goal, and consumers have absolutely no clue that GET /v1/sources and GET /v1/sources/{id}/destinations can only be used with POST /v3/transfers. That is definitely not consumer-friendly.

Now let’s look at the last level of versioning: the data/message level. Figure 9.14 shows what could happen to the transfer money goal using this versioning strategy. Note that this strategy only works on the data that is located in request and response bodies; headers and query parameters are out of its scope.

09-14.png

Figure 9.14 Data (or message) versioning with content negotiation

Because the Banking API uses the HTTP protocol, we can take advantage of the content negotiation feature to version the requests and responses for each goal. In the API’s initial version, POST /transfers requests and responses use the application/vnd.transfer.request.v1+json custom media type. When the transfer money goal’s input is modified, its media type’s version bumps to v2 (application/vnd.transfer.request.v2+json), then v3 (application/vnd.transfer.request.v3+json). The response version is modified only on the second breaking change to v2.

In both cases, the versions of requests and responses are not correlated anymore, and with the second update, it becomes unclear which versions of requests and responses work together. Note that we could perfectly correlate request and response message versions by simply bumping request’s and response’s versions together no matter which one is modified. In that case, this strategy is close to the goal/operation versioning strategy. Table 9.3 sums up the pros and cons of each level of granularity.

Table 9.3 Choosing a versioning granularity for REST APIs

Granularity Pros Cons Recommended?
API No need to think about which versions of operations or resources work together API version change on single breaking change, no clue about the changes By default for REST APIs (common practice)
Resource Gives hints about changes Impossible to guess which versions work together Not recommended for REST APIs; use only when resources are completely independent
Goal/operation Indicates which goals have changed Impossible to guess which versions work together Not recommended for REST APIs; use only when operations are completely independent
Data/message Indicates which data/messages have changed Impossible to guess which versions work together; limited to request/response bodies when using HTTP Not recommended for REST APIs; can be used in conjunction with API-level granularity

Each level of granularity has its pros and cons, but at least in REST API world, the most commonly used strategy is API-level versioning. Choosing any other granularity must not be done lightly because most consumers are not used to these versioning strategies. But that does not mean that they must never be used, especially if the API you are designing is not a REST one.

You might sometimes need to mix different versioning granularities. For example, if you work in the banking industry, you might have to work with the ISO 20022 standard that defines XML (and soon, at the time of writing of this book, JSON) messages. Messages come in versioned request/response pairs. If you were to design an API using these messages, you would have to deal with the versioning of both your API and the ISO 20022 messages.

API versioning should hold few secrets for you now. But as an API designer, you must be aware of its impact beyond API design.

9.2.4 Understanding the impact of API versioning beyond design

What is discussed here is mostly the concern of API product managers, tech leads, and architects; but as API designers, it’s good to be aware of these matters too (plus, sometimes APIs designers have more than one role). Even if the changes introduced in the API are not breaking ones, each of them must be carefully recorded so that you are able to communicate the list of changes to consumers.

You should understand by now that changing the version of an API—or more precisely, introducing a breaking change—has consequences on the consumer side, and that consumers might not be happy with that. Creating new versions of an API means that multiple versions of the API will run at the same time, and consumers might not be willing to make the effort to switch to a newer version if the older one they are using is still running. Therefore, the breaking changes that are introduced in the API have to be carefully chosen.

For example, introducing a breaking change that does not bring any value to the consumers, such as switching from an OAuth 1 to 2 security framework, is definitely not a good idea. To make the switch less inconvenient, it would be better to introduce new features that consumers want along with such boring breaking changes.

Regarding implementation, having to expose multiple versions of the API might require extra work and, therefore, choosing how many versions will be supported and for how long is important. This depends on your context. Some API companies providing their services only as an API might choose to indefinitely support all versions. On the other hand, for a private API, some companies might only support two versions. There is no silver bullet; it’s up to you to choose an adapted solution.

On the technical level, there are broadly two options to manage versioning in the implementation. The first option is for each version to be handled by a specific implementation. This means development of each older version of the implementation will go on (at least to fix bugs and security issues, for example) for as long as those versions remain in use. The infrastructure supporting each older version must also be maintained. Depending on the context of your company, this may or may not be a problem. This context will definitely have an impact on how long you let consumers use older versions of an API. The second option is for all versions to be handled by a single implementation. Again, depending on the context, having one implementation manage all possible versions of your APIs may or may not be a problem.

As you can see, versioning can be challenging. Are there ways to lessen the risk of breaking changes that necessitate a change of API version?

9.3 Designing APIs with extensibility in mind

We know how to avoid introducing breaking changes when possible and how to version APIs when such changes are unavoidable. That’s good, but we must not forget one of the fundamental principles of software design: extensibility.

“Extensibility is a software engineering and systems design principle where the implementation takes future growth into consideration. The term extensibility can also be seen as a systemic measure of the ability to extend a system and the level of effort required to implement the extension. Extensions can be through the addition of new functionality or through modification of existing functionality. The central theme is to provide for change—typically enhancements—while minimizing impact to existing system functions.”

Wikipedia

By carefully designing data, interactions, and flows, and choosing the appropriate level of granularity for versioning, we can design extensible APIs that facilitate evolutions and, more importantly, lessen the risk of breaking changes.

9.3.1 Designing extensible data

Figure 9.15 shows how to design data envelopes in order to make an API extensible.

09-15.png

Figure 9.15 Choosing extensible data envelopes

What do you think will happen if the transfer money goal just returns the money transfer’s ID, such as "T775688964", as a raw string? At first, consumers might be puzzled because they get a response whose Content-type is text/plain instead of the usual application/json or application/xml used in all the other goals. That’s awkward, but they might get used to it … until the Banking Company decides to return the entire resource that was created in order to avoid many subsequent calls to the list transfers goal. Instead of a text/plain response containing a raw string, they now get an application/json response containing an object. That’s a breaking change. If the response had been an object containing an id property from the start, adding the other transfer properties wouldn’t be a problem at all. The same goes for the list transfers goal returning a list of transfer IDs as strings.

Speaking of lists, returning one of those is not a good idea either. What would happen if metadata had to be added in order to provide information about pagination, such as the total number of items? It’s a breaking change, again. The way to avoid that is by encapsulating the list in an items property inside an object.

So, as you can see, all high-level data (the resources in a REST API) must be enveloped inside an object to ensure extensibility and lessen the risk of breaking changes. But what about the data inside this envelope? As figure 9.16 shows, you should beware of Booleans and provide self-descriptive data.

09-16.png

Figure 9.16 Choosing types wisely and using self-descriptive data

When a money transfer is created using the transfer money goal, it is not executed immediately. In order to provide information about this transaction, there’s an executed Boolean property that is true when the money transfer is executed and false otherwise. What happens if a new state is introduced?

Let’s say that some money transfers need to be validated before execution for some reason. How do we handle this? We can add a validated Boolean property to signify this. But then what happens if a third postponed state is introduced? Should we add another Boolean property? Adding these new properties in a response does not introduce a breaking change, but consumers won’t be aware of these new states if they don’t update their code.

To avoid adding multiple Boolean status properties, we can instead add a single status property. This allows us to add new statuses as needed without adding new properties. This status could be a number or a string. Note that a Boolean is less extensible than a number, which is less extensible than a string. But as you saw in section 9.1, adding values to an enum could provoke a breaking change. Making the status a self-descriptive object with a code and an easily interpretable label might lessen this risk.

So choose your property types carefully in order to ensure extensibility, and always think about providing self-descriptive data in order to lessen the risk of breaking changes. All this works only if a single property is sufficient to replace multiple ones. Figure 9.17 shows what to do when it is not.

09-17.png

Figure 9.17 Grouping similar data in a list

A money transfer has creationDate and executionDate properties, corresponding to the dates when it was created and then executed. To provide information about the new validation state you have just seen, a validationDate property could be added. But then the same problems we have just seen would arise. These different dates could be replaced by an events list property, its elements each consisting of a date and a status (EXECUTED for the execution date, for example). Adding new dates to the list is quite simple; and, of course, the status could be provided in a self-descriptive format.

If the properties are similar, always consider whether you can put them into a list, possibly using self-descriptive data, which will facilitate the addition of elements and lessen the risk of breaking changes. Speaking of self-descriptive formats that lessen the risk of introducing breaking changes, figure 9.18 shows how we can use standards to design extensible APIs.

09-18.png

Figure 9.18 Using standards and a wider range of self-descriptive values

A proprietary nomenclature could be used to describe a recurring money transfer period. The values could be ready-to-use ones like MONTHLY or QUARTERLY, but adding a new value such as WEEKLY would inevitably introduce a breaking change. This approach is also quite rigid. What if a customer wanted to trigger a money transfer every 10 days? You’d have to add a new value. Using the ISO 8601 duration format might solve the problem. It can describe any duration using a simple format; for example, P1M corresponds to MONTHLY and P10D corresponds to 10 days.

In a similar manner, you’ve already seen the benefits of using ISO 4217 currency codes to facilitate not only understanding but also extensibility. If the Banking API needs to manage new currencies, consumers will be able to understand these easily because they understand ISO 4217. So using standards and a wider range of self-descriptive values instead of a finished list facilitates extensibility and lessens the risk of breaking changes.

9.3.2 Designing extensible interactions

Postel’s law states

“Be conservative in what you do, be liberal in what you accept from others.”1 

1 This is often reworded as “Be conservative in what you send, be liberal in what you accept.”

Postel's law (Robustness principle)

Applied to API design, the robustness principle could be understood as “Be consistent in what you return and try to avoid errors.” You saw how to be consistent and ensure extensibility regarding the data returned by an API in section 9.3.1, so let’s focus on errors.

Concerning error data and being consistent in what we return, we can apply what we have learned in order to be as generic as possible. You saw in section 5.2.3 that to provide informative feedback, we can type errors as shown in the following listing.

Listing 9.2 An informative error message

{
  "errors": [
    { "source": "amount",
      "type": "MISSING_MANDATORY_ATTRIBUTE",
      "message": "Missing mandatory amount" }
  ]
}

Because this type is generic, we can reuse it for another property that becomes mandatory. If the type were MISSING_AMOUNT, we would not be able to reuse it and would instead be forced to introduce a new type of error that consumers would not be able to interpret without updating their code. In general, the more generic the type values are, the more extensive the error feedback is.

As for trying to avoid errors, what would happen if a consumer provided an unknown test=2 parameter when requesting to list transfers? The API could be strict and return an error saying, “Sorry, we do not understand the test parameter.” Providing informative error feedback is consumer-friendly, but the API could also simply not take this unknown parameter into account, process the request, and return a result. That is still a consumer-friendly design, but also an extensible one. Indeed, if this test parameter had actually existed in a previous version, non-updated consumers might still send it, and it would bother them that their queries now trigger some unexpected error. Note that works only if ignoring test actually has no negative side effect on the consumer side.

Let’s consider some other types of errors. What should the Banking API do if a consumer sends a pageSize=150 parameter with a list transfers request, but the maximum size of a page is 100? For the same reasons, the API should not return an error but a page of 100 elements. Then, if one day (perhaps for performance reasons) the maximum size is reduced to 50, no consumers will be bothered. The pagination metadata should provide all needed information in order to let consumers seamlessly use the modified goal; but if necessary, some warning metadata could be added along with the response using the same format as errors to signify the modifications made to the requests.

And what should the Banking API do if a consumer sends an amount of 15000, which is above the maximum transfer amount of $10,000 (regardless of source account balance and owner privileges)? Should we trigger a $10,000 money transfer instead of a $15,000 one? Obviously not! As API designers (and implementers), we should try to avoid returning errors, but not at all costs.

As you can see, this is the implementation’s business. But as an API designer, you’ll have to define a policy regarding errors and unknown or invalid parameters (query parameters, headers, or properties in bodies). Will you not take the issue into account and use a default value to lessen the risk of breaking changes? Or will you be strict and return errors to be more secure and favor consumer accuracy (if they break, they will update)? Your approach will depend on the context of the API and the context of each goal.

9.3.3 Designing extensible flows

How you design each goal in a flow and the flows themselves will impact the extensibility of your API. The Banking API was initially created for the Banking Company’s mobile application. With this application, end users making a money transfer have to select a source account, then a destination, and then provide an amount.

From the API’s perspective, this means using the list sources goal to list possible sources of a money transfer. After that, list destinations can be used to get the accounts and registered beneficiaries that can be used as the destination for a money transfer using a specific source’s ID. Finally, the transfer money goal can be used to make the transfer with a provided amount and source and destination IDs.

Now suppose some people within the Banking Company decided to build a money transfer tool for the back office. They were quite happy when they discovered that there already was a money transfer API. In their implementation, they already had the source and destination account numbers, so they simply called the transfer money goal using these values. Unfortunately, all their calls ended with an “Unknown source account” error.

After some investigation, they realized that the transfer money goal expected source and destination IDs, which were not regular account numbers. They had to call list sources to find the ID corresponding to their source account number and then follow the same flow as in the mobile application. What a pity. If the flow had not been so focused on the mobile application use case, and if the various goals involved in the money transfer flow had used regular account numbers, it would have been far simpler.

As you can see, extensibility in design is not only about ensuring that modifications can be done with a low risk of breaking changes. Extensibility is also about ensuring that the API can be used in a wide range of use cases, not only the one it was originally created for.

Always try to see beyond the specific use case you are working with and ensure that the flows you design, especially UI flows, are not correlated to a specific process. Also, try to design each step so it can be used in a standalone way. Choosing widely adopted inputs and outputs, especially IDs, helps to achieve that.

9.3.4 Designing extensible APIs

Last but not least, how can we ensure extensibility at the API level? What will happen if the Banking API grows to provide several dozen goals covering various topics such as account information, banking services subscriptions, money transfers, and personal finance management, among others? Obviously, breaking changes will occur, even if all the design principles we have seen so far are applied. Why? Simply, because it is big!

The bigger an API gets, the higher the number of evolutions, and therefore, the higher the risk of breaking changes. That’s quite simple to understand, and the solution is obvious: instead of building big APIs, we should build smaller ones.

It’s not always that simple to define relevant groups of goals that can be combined easily, however. This is not specific to API design; it’s a common challenge in software design. Hopefully, if you remember section 7.2.3, you already have the basics that can help you to organize goals and split an API into smaller parts. You will also have to analyze each goal you add to an existing API in order to evaluate whether the goal you are adding should instead be part of a different API using the same principles.

Summary

  • Each API evolution must be carefully designed in order to avoid breaking changes, which can cause problems not only on the consumer’s but also the provider’s side.
  • API designers might have to live with previous poor design choices in order to avoid introducing benevolent but breaking changes.
  • Depending on the context, breaking changes might be acceptable (for example, private APIs with consumers under the organization’s control).
  • API versioning is a design + implementation + product management matter.
  • Designing APIs with extensibility in mind eases the design of evolutions, lessens the risk of breaking changes, and favors API reusability.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset