You have to be really consciously careful about API design. APIs are forever. Once you put the API out there, maybe you can version it, but you can't take it away from your customers once you've built it like this. Being conservative and minimalistic in your API design helps you build fundamental tools on which you may be able to add more functionality, or which partners can build layers on top of…
Managing change is not easy, yet it is an inevitable part of maturing an API. For developers working within a single codebase, change can be difficult but is manageable. Refactoring tools and automated test coverage are leveraged to assess the impact of a change.
When the change involves web-based APIs, change becomes even more challenging. Some teams may have a direct relationship with every API consumer, allowing for changes to be introduced gradually and in coordination with all parties. However, that is usually not the case. Instead, most consumers of an API have no personal relationship with the team that owns the API. Extra care is required to manage changes to an API design to avoid customer churn. This chapter presents some considerations to determine the impact of change and strategies to introduce change to API designs that minimize the impact to API consumers.
The ADDR Process will work for any organization, whether an early-stage startup or an organization with hundreds of existing APIs. The process surfaces the outcomes and activities needed by customers, partners, and the workforce. This approach is useful whether a team is designing their first or 50th API.
The fictional online bookstore example used throughout this book assumed that the APIs identified throughout the process did not already exist, resulting in a greenfield project. The reality is that organizations already have APIs in production for a variety of purposes and that any proposed API designs must fit the reality that brownfield development will be required.
These brownfield initiatives are forced to reconcile the findings from the ADDR Process with any existing API designs to determine the best path forward. This section details some considerations for handling change when APIs already exist.
Teams should perform a gap assessment of the ideal API design identified during the process with the way it is designed today. The team must then determine if they will follow the same style and design decisions of the API design for consistency, mix the new design alongside the older design decisions, or consider other alternatives.
Factors to consider when performing this design gap analysis include:
■ Introduction of differing terminology for resources and resource properties during the design process
■ A shift from data-centric to resource-centric API design styles
■ Change in vision and direction for the API product compared to what exists today
Using these factors as a starting point, itemize the differences between existing APIs and the ideal API design created as a result of the ADDR Process. Assign a sizing for the value provided to API consumers by the new API design and the size of the impact in API design change. Using t-shirt sizing (e.g., small, medium, large, extra-large) ensures the measurement is an effective way to size the value and impact. Then determine what is best for API consumers.
Making an API design decision, particularly when breaking changes will be required, involves more than the direct impact to the organization. It must also include what is best for API consumers.
Consider the following questions to determine the impact of API changes to current and future API consumers:
■ Who are the API consumers? Internal consumers may offer easier change coordination, while partners that will be resistant to making changes to integrations. Customers and third parties acting on behalf of customer may be unable to make changes due to limited or no development resources available.
■ What kind of relationship has been established with API consumers? An internal or external party that the team knows personally can more easily negotiate for breaking changes. API consumers that have no relationship may be more challenging. API consumers that are heavy influencers in the marketplace may have a negative impact on current and future customer prospects if they are cornered into adopting unnecessary API changes.
■ What value is being delivered to API consumers as a result of the change? API changes that improve the use of the API may be well-received. It may also unlock new capabilities that they have been requesting, even with the cost of change. For others, it may give them pause to consider moving to a different vendor resulting in customer churn.
How an organization manages change with their API consumers tells a lot about who and what they value. If the API provider prefers to deliver API design elegance at the cost of constant breaking changes, API consumers may soon start shopping for alternatives. If, however, the API provider values the API consumer above having the perfect API design, they may just find themselves the leader if the marketplace.
Proceeding with an existing design style may require compromises in the API design that are less than ideal. These compromises may include minor annoyances, such as continuing forward with a misnamed resource that don’t necessarily reflect the insights gained during the ADDR Process.
Compromises such as supporting the old message formats alongside the new are commonly found in the real world. In this case, the server first checks for the new request message format, falling back to the older message format when necessary. This places the versioning responsibility onto the server rather than the API consumers, ensuring that no breaking changes are introduced.
Another example is the mixing the new design style alongside the older style. New operations use the new design style while older ones remain as-is for a time. Gradually, older operations are replaced by newer operations using a deprecation strategy that encourages existing integrations to migrate to the new operations one-at-a-time.
However, some compromises may be more significant, such as an existing API design that is too low-level. This is common for APIs that opted to expose database tables directly compared to the new proposed API design that would apply a more course-grained design with an outcome-based focus. Mixing low-level and high-level APIs may create too much cognitive dissonance for developers and therefore are less than ideal.
Teams will need to determine if they wish to add the new design to an existing API, start a new API as if it were a brand-new product offering, or deliver the new design as a new version of the existing API. Each option will have an impact both on the organization and on current and future API consumers.
If the existing API design impedes the API consumer’s ability to use the API effectively, a more greenfield approach may be required. Keep in mind that if the team chooses to release a new API product or version, additional resources will be required to maintain both APIs for some amount of time in the future. The next section discusses API versioning strategies and considerations.
The most important thing is that the ADDR Process helped to align business and API teams in a unified understanding of the problem space. This may create a vision of the design target state of the API design and reality. Work through the process using the recommendations provided in this chapter. This will ensure that enhancements to an existing API or the delivery of a new API meets the needs of the API consumers while ensuring that the trust between API provider and consumer is not lost.
Principle #5: APIs are forever, so plan accordingly
Thoughtful API design combined with an evolutionary design approach makes APIs resilient to change. Extra care is required to manage changes to an API design. This helps to avoid frustrating developers that are required to stay updated with the latest changes.
APIs are contracts established between the providers of an API and their consumers. Ideally, they will never have to alter this contract. However, that may not be reality. There may be times when a change to the contract is required. When this happens, teams should try to ensure that they do not introduce breaking changes that will force their API consumers to fix code. For some API consumers, updating code to adapt to an API change may not be an option at all. Therefore, it is important to understand what may constitute a breaking change, then establish an API versioning policy that encourages the evolution of an API over time without breaking existing API consumers.
Non-breaking changes tend to be additive in nature, although this isn’t always the case. These kinds of changes may include:
■ Adding a new API operation. Existing client code won’t use the operation, so no harm is done to existing integration.
■ Adding an optional field to a request message. In this case, existing client code will not be forced to add the new field.
■ Adding a required field to a request message with a default value. For client code written prior to the addition, the server will apply the default value since it would be missing from the request.
■ Adding a field to a response message. Existing client code should safely ignore the new field(s), unless they opted to use a mapping library that raises an error if the newly added field cannot be found in the destination object. This is an anti-pattern for API consumption but may be encountered in some circumstances, so use caution.
■ Adding a value to an enumeration field type. New enum values that are deserialized on the client may not have a known display string associated to it. To be a non-breaking change, older clients must run correctly when receiving a new enum value. Not all clients may be designed in this way, so caution is advised.
Changes that are incompatible with existing integration code include, but are not limited to, the following:
■ Renaming fields and/or resource paths, as existing client code will require a code change to adapt to the renamed value.
■ Renaming or removing fields in a request or response
■ Removing API operations used by existing API client code
■ Changing fields from a single value to a one-to-many relationship. For example, moving from one email address per account to a list of email addresses for an account.
■ Changing the HTTP method or the response codes returned by an API operation
Remember that once an API is released in production and has at least one integration, the design decisions are permanently a part of the API. This is why the ADDR Process is so important as it helps teams to validate design decisions before an API goes into production. However, a proper API versioning strategy can assist in mitigating some of these issues while allowing an API design to evolve over time.
There have been many discussions, articles, and debates about API versioning. The most critical aspect of every discussion must be the differentiation between safely evolving an API design and introducing breaking changes that force code modification as a result. One of the biggest tools in the API versioning toolbox is the introduction of API versions and revisions.
API versions represent a grouped set of API operations. Within each version, all modifications to an API should be backward compatible. However, across versions there is no guarantee to be compatible. Each version of an API is often treated as a separate product with differing behaviors and capabilities. API consumers opt into a specific version and write code against that version. They only migrate to a new version when they are ready, which may be a long time after the new version is released, or perhaps never. Versions may be numbers or strings (e.g. v1 or 2017-01-14). For those familiar with semantic versioning (semver), this is the same thing as a major version. Figure 14.1 illustrates two versions of an API, each offered as a different product that the API consumer selects when making the API request.
An API revision identifies an internal enhancement that should have no negative impact to API consumers of a specific API version. Revisions should be transparent to the API consumer, as consumers should be subscribed to a specific version only. The provider opts to release a new revision of a specific API version with or without the knowledge of the API consumer. Internally, a team may release v1.5, but for API consumers, they only know that they are using v1 of the API. API consumers may review changelogs for each revision to see if an enhancement would be useful, but otherwise take no action when the provider releases a new API revision. This is equivalent to increasing the minor version number when using semver.
There are three popular methods of implementing API versioning: header-based, URI-based versioning, and hostname-based versioning.
Header-based versioning places the desired version as part of the
Accept header in the HTTP request, e.g.,
Accept: application/vnd.github.v3+json. This considered the preferred form of versioning by many, as the URI remains the same across versions while the media type defines which version of the resource representation is desired.
URI-based versioning includes the version as part of the URI, either as a prefix or suffix. Examples include
/v1/customers. This method of versioning tends to be the most encountered as it works across a variety of tools that may not support customizing request headers. The downside is that resource URIs change with each new version, which some consider counter to the intent of supporting evolvability through a resource URI that never changes.
Hostname-based versioning includes the version is part of the hostname rather than the URI, e.g.,
https://v2.api.myapp.com/customers. This approach is used when technology limitations prevent routing to the proper backend version of the API based on the URI or
Accept request header.
No matter which option is selected, API versions should only include the version number. Minor numbers should not be used, otherwise code changes are required and thus cause typical non-breaking changes to become breaking changes. For example, code changes would be required to move from /
/v1.2/customers, even if the only difference between versions is the addition of a new operation.
Each time a new version is released, customers must decide whether they wish to opt-in or not. The decision is based on the cost vs. the reward. Is the effort to migrate worth the cost it will take to migrate?
Moving to a new API version is a forcing factor. Just because an API design isn’t perfect doesn’t mean the team should release a new version with breaking changes to get the design exactly right. Every time a new version is released to accommodate a desired breaking change, the organization risks introducing customer churn. That is because the customer must weigh the cost of migration vs. moving to a competitor.
In addition, introducing a new version often requires keeping the current API version around for some indeterminate period of time. While some organizations may have the leverage to force customers to upgrade by some period of time, this isn’t always the case. This means supporting any previous versions of an API for the foreseeable future.
Every new API version is like a completely new product that requires additional infrastructure, support, and development costs to maintain. Keep this in mind when the temptation arises to release a new API version to fix that annoying design decision that crept in last year.
Nothing will ruin a team’s week more than scrambling to find a replacement to an API that has been shut down overnight. To avoid this kind of impact on existing API consumers, teams must define their deprecation policy and communicate it to their API consumers.
Deprecating an API operation or product provides an opportunity for an API provider to maintain a level of trust with their API consumers. But this requires a clear policy and planning to deprecate and eventually sunset an API. When executed properly, API consumers are notified early and often of the deprecated API and given a chance to move to an alternative solution prior to the retirement of the API.
The organization should have a clearly documented deprecation policy and process as part of the API program standards and practices. This policy should include:
■ Details on when deprecations are allowed
■ Steps to establish a deprecation process
■ The minimum duration of the deprecation period prior to retirement of the API or operation
■ A requirement to establish a migration path for consumers, even if that includes other vendors that provide a solution similar to the deprecated operation or product
■ A clear definition of the organization’s deprecation policy in the API’s terms of service
Organizations that establish a deprecation policy will be better equipped to deprecate an API operation or product while maintaining the trust of their API consumers.
Communicating a deprecation is a significant factor in maintaining API consumer trust. The methods of communication vary, but should include:
■ Well-written emails that address the deprecated operation(s) or product(s)
■ Notification banners at the top of a web-based dashboard
■ Warnings embedded in all related API documentation
■ Blog posts or a dedicated landing page for deprecations that discuss the decision and address frequently asked questions
■ Frequent social media notifications with a link to the blog post
The announcement strategy should include most, or all of the methods listed above. Keep in mind that employee turnover may result in email addresses that are not current, so using a variety of methods ensures the most effective communication possible.
For APIs using OpenAPI descriptions, use the
deprecated: true indicator, resulting in the rendering of deprecation warning in generated HTML documentation. GraphQL and gRPC-based APIs have similar provisions for their schema and IDL formats.
Use the Sunset Header RFC to programmatically communicate when it will be retired. Consider including a step-by-step guide of using the Sunset Header as part of the API documentation will help API consumers receive automated notification of deprecated API operations.
Finally, if API helper libraries are offered, include a warning to the log file or console regarding the existence of the Sunset Header. Backend code that uses the library may redirect log files to log aggregators, resulting in internal alerts in monitoring dashboards.
As the quote at the start of this chapter indicates, an API design isn’t finished until someone is using it. So, how can teams design an API that lasts forever if the design isn’t finished until after it is being used? This requires a few disciplines that encourage evolutionary API design, including listening to early feedback, continually seeking additional insights, and establishing expectations through an API stability contract with developers.
The ADDR Process is designed to engage with stakeholders early and often to ensure that the API design meets the needs of its target audience. API designers must be willing to listen, learn, and adjust their API designs according to the feedback they receive. Anything short of this will only serve the needs of the API owners rather than serving the needs of current and future API consumers.
Not only must teams listen to feedback during the initial design process, but they must also continue after the initial release. Seek to better understand how the API is being used beyond its original intent. Conduct interviews with customers and developers to see how API design and documentation improvements can help them. Communication must be continuous rather than a one-time discussion at the start of the API design process.
An API stability contract is a method of establishing expectations of change between the API provider and their API consumers. The contract defines the level of support and longevity API operations or entire API-based products. Below is a recommended starting point for organizations:
■ Experimental: This is an early release for experimentation and feedback. There is no guarantee that it will ever be supported. The design may change, or it may be removed completely in a future release
■ Pre-Release: The design has been pre-released for feedback and will be supported in the future. However, the design is not frozen and therefore may introduce a breaking change
■ Supported: The API is in production and supported. Any design changes must not break existing consumers
■ Deprecated: The API product or API operation(s) is still supported but will soon be retired
■ Retired: No longer available or supported
Applying an API stability contract gives API providers the freedom to introduce new API operations or experimental APIs early for design feedback, prior to supporting it long-term.
Changes to API design cannot be avoided. Whether internal or external, consumers depend upon the API to remain stable in the face of improvements. Introducing changes to an API’s design provides an opportunity for the owning team and the organization to maintain the trust of their API consumers. By applying an appropriate API versioning strategy, taking appropriate steps to deprecate APIs when they are no longer needed, and establishing an API stability contract, teams are able to manage change while avoiding negative impact to their API consumers.