Microservices will change. This is inevitable.
But how do you manage that change to ensure your consumers don’t feel unnecessary disruption? Alternatively, what best practices can you follow to make migrations easy?
In this post, we’ll cover microservice and API versioning. We’ll divide the discussion into two parts. First, we’ll cover versioning of the actual microservice itself, and then we’ll cover API versioning. Both provide your service consumers with improved compatibility and rollout of new features.
So let’s get started.
First, let’s talk about versioning the actual microservice as it’s rolled out across your ecosystem.
Now, you may realize that having multiple versions of microservices running in your cloud environment adds complexity. You want to reduce this as much as possible.
However, rolling out new versions of a service across all regions and consumers increases risk. If there’s a problem with the latest version of the service, you want to limit the fallout and ensure that most of your customers don’t feel the pain.
Deployment Practices and Versioning
If you’re already working in a complex ecosystem of microservices or just getting started with one, you may realize that the best way to roll out new versions of a service involves gradual deployment.
To meet that need, we use various deployment practices like canary deployments, blue-green deployments, or even A/B testing deployments. These deployment styles ensure gradual rollout but inevitably require more than one version of a service to run in production at once.
Most of the time, this works great—especially if your microservice teams follow good API deployment practices. (We’ll discuss those in the next section of this post.) However, one thing that we often lack in these environments is visibility.
We can’t see what service versions are running where. Or what other services consume them.
Even if it’s just for five or ten minutes or a few hours, having multiple program versions to track in production can complicate debugging and monitoring. Fortunately, tools like OpsLevel’s deployment tracking can help visualize what’s running and where.
Reduce Versioning Issues
So assuming that we’re in an environment that deploys microservices gradually, how can we best prepare, knowing that multiple versions of our code will be deployed across our infrastructure?
These three practices will help reduce microservice versioning issues.
First, consider backward compatibility. Whenever adding, removing, or changing features to your microservice, look for ways to make that change backward compatible with previous consumers of your service.
Second, use feature flags. Feature flags will allow you to separate the act of deploying a service from the release of new features.
Therefore, you can deploy new versions of code continuously without introducing feature changes until you want to. Those feature flag rollouts should be rolled out gradually as well.
And third, version your APIs.
You may think, “Hey, this is a small internal service. We don’t need versioning.” However, even if you believe your particular microservice won’t change much, you’re probably wrong.
Alternatively, you may think that because you’re using GraphQL or a messaging platform like Kafka, you won’t need to worry about versioning. Whether you’re using REST, gRPC, messaging, or, yes, even GraphQL, the schema will change, and you need to be ready for that change.
Most of this post will cover topics related to REST, but much of our advice applies to other formats as well.
But first, in a post about best practices for managing change, let’s talk about how to reduce change in the API contract to reduce the frequency of version changes.
Just because we can version an API does not always mean we should. In fact, we should reduce making changes to existing APIs as much as possible.
Reducing Version Changes
As hinted at above, though having a versioning strategy is important, reducing the need for version changes can be more important.
Why is that? Well, just because we can version an API does not always mean we should. In fact, we should reduce making changes to existing APIs as much as possible.
When adding new versions, we want to reduce the burden of maintaining old versions.
If we push fewer versions, we’ll have less code and complexity to maintain. Furthermore, constantly publishing new code increases the burden on our consumers of having to change to new versions with potentially large schema changes. We want to keep things easy for our consumers.
Whether we’re adding new functionality, removing or replacing old functionality, or changing the relationships between models, we will need changes in our APIs. So what are some ways we can help reduce changes in our API?
When creating APIs, consider their current use and their most likely future use. This can be tricky as we don’t want to over-engineer our design. However, if we know what’s coming down the road, we should plan for that in our API design.
The makers of GraphQL strived to make an evolving API that avoids breaking changes. Even so, there’s nothing to stop us from breaking an API by removing or changing fields and models.
However, since you only serve requested data with GraphQL, changes like adding new resources, types, and fields provide greater safety than REST models.
So what can we change on REST APIs that evolve the functionality but typically don’t break consumers? In other words, what can we change that includes backward compatibility?
- Add additional elements to the response—with a caveat. Many consumers of API parse responses in ways that ignore unknown or unexpected fields. However, some consumers break when unexpected fields appear in the response.
- Make an input parameter optional with a default value.
- Add a new optional input parameter with a default value if needed.
When adding to existing models or adding functionality, use the tips above to evolve the code without pain for consumers.
However, also make it clear that you may make these changes. Documentation should always state your versioning practices and specify what types of changes may occur without versioning the API.
Now let’s look at changes that don’t provide backward compatibility. These changes break API contracts and should involve version changes.
- Remove or rename an endpoint.
- Remove a field or type from the response.
- Change an input parameter to required.
- Add a new required input parameter.
All these changes require a version change as consumers will have to plan for and move to a new version to take advantage of new functionality.
How to Version a Contract
Now that we’ve covered some practices around the API contract, let’s look at different ways of versioning that contract.
First, you can specify the version in several ways.
- URL path: “api.my-service.com/v1/catalog”
- Path param: “api.my-service.com/catalog?version=1.2”
- Accept header: Accept:application/catalog.v1+json
- Custom header: My-Service-Version:2.3
To choose the best method for your org, consider how you’re going to route the request. Will it be inside or outside the service? How might the result be cached on the consumer’s side? And what are consumers expecting?
Additionally, you can choose different formats for the actual version.
- Major version: v1, v2, v3
- Dated version: v2021-12-01
- Semantic version: 1.4.12
Since these are APIs that should not change versions often, I would recommend simpler versions that only indicate breaking or significant changes. If you find yourself needing semantic versioning for your API, you may be versioning too frequently or at too fine-grained a level.
Remember, you don’t have to change versions unless the contract changes.
Overall, what you choose from the lists above matters less than applying it consistently.
For example, let’s say you have external consumers of your APIs. If each API or microservice had a different versioning strategy, it would make it difficult for consumers to get things done and stay on top of changes. Make it easy for them. And make it easy for your development teams to understand the practices that your org follows.
Encouraging Upgrades or New Version Adoption
Now that we know how to version our APIs, how do we encourage consumers to switch to a new version? We don’t want old versions to live on indefinitely, so what do we do?
These tips can also be part of your versioning strategy to move consumers to newer versions sooner.
- Document which fields or functionality are deprecated and will be removed. The docs should also let consumers know how to upgrade or how to achieve the previous functionality in the new API version.
- Responses should send information on deprecation. GraphQL has this built in, though it doesn’t tell the consumer when the deprecation will occur. This method only works for APIs where devs actively look at the response. Services that have been using your API for a while might not have anyone watching for the deprecation warning.
- Provide clear communications around timelines.
- Monitor traffic to versions of APIs so that you can identify consumers that are lagging and safely turn off old versions.
- Give consumers a reason to upgrade, like improved features or better performance.
- Keep old code versions patched for security concerns, but do not add any new backward-compatible features to old versions.
Using the guide above, decide what’s best for your team, org, and product.
You’ve learned how microservice versioning and API versioning can work together to provide new functionality for your consumers. Next, take a look at how OpsLevel can make microservice management and discoverability easy through their microservice catalog.