https://PROJECT_ID.REGION_ID.r.appspot.com
If you deploy a service named user-service
, you can access the default serving version of that service using the following URL:
https://user-service-dot-my-app.REGION_ID.r.appspot.com
If you deploy a second, non-default code version named banana
to the user-service
service, you can directly access that code version using the following URL:
https://banana-dot-user-service-dot-my-app.REGION_ID.r.appspot.com
Note that if you deploy a second, non-default code version named cherry
to the default
service, you can access that code version by using the following URL:
https://cherry-dot-my-app.REGION_ID.r.appspot.com
App Engine enforces the rule that the names of code versions in the default service cannot collide with service names.
Direct-addressing specific code versions should only be used for smoke testing and to facilitate A/B testing, rolling forward, and rolling back. Instead your client code should address only the default, serving version of either the default service or a specific service:
https://PROJECT_ID.REGION_ID.r.appspot.com
https://SERVICE_ID-dot-PROJECT_ID.REGION_ID.r.appspot.com
This addressing style allows the microservices to deploy new versions of their services, including bug fixes, without requiring any changes to clients.
Every microservice API should have a major API version in the URL, such as:
/user-service/v1/
This major API version clearly identifies in the logs which API version of the microservice is being called. More importantly, the major API version yields different URLs, so that new major API versions can be served side-by-side with old major API versions:
/user-service/v1/ /user-service/v2/
There is no need to include the minor API version in the URL because minor API versions by definition will not introduce any breaking changes. In fact, including the minor API version in the URL would result in a proliferation of URLs and cause uncertainty about the ability of a client to move to a new minor API version.
Note that this article assumes a continuous integration and delivery environment where the main branch is always being deployed to App Engine. There are two distinct concepts of version in this article:
Code version, which maps directly to an App Engine service version and represents a particular commit tag of the main branch.
API version, which maps directly to an API URL and represents the shape of the request arguments, the shape of the response document, and the API behavior.
This article also assumes that a single code deployment will implement both old and new API versions of an API in a common code version. For example, your deployed main branch might implement both /user-service/v1/
and /user-service/v2/
. When rolling out new minor and patch versions, this approach allows you to split traffic between two code versions independently of the API versions that the code actually implements.
Your organization can choose to develop your /user-service/v1/
and /user-service/v2/
on different code branches; that is, no one code deployment will implement both of these at the same time. This model is also possible on App Engine, but to split traffic you would need to move the major API version into the service name itself. For example, your clients would use the following URLs:
http://user-service-v1.my-app.REGION_ID.r.appspot.com/user-service/v1/ http://user-service-v2.my-app.REGION_IDappspot.com/user-service/v2/
The major API version moves into the service name itself, such as user-service-v1
and user-service-v2
. (The /v1/
, /v2/
parts of the path are redundant in this model and could be removed, though they still may be useful in log analysis.) This model requires a bit more work because it likely requires updates to your deployment scripts to deploy new services on major API version changes. Also, be aware of the maximum number of allowed services per App Engine application.
It's important to understand the difference between a breaking change and a non-breaking change. Breaking changes are often subtractive, meaning they take away some part of the request or response document. Changing the shape of the document or changing the name of the keys can introduce a breaking change. New required arguments are always breaking changes. Breaking changes can also occur if the behavior of the microservice changes.
Non-breaking changes tend to be additive. A new optional request argument, or a new additional section in the response document are non-breaking changes. In order to achieve non-breaking changes, the choice of on-the-wire serialization is essential. Many serializations are friendly to non-breaking changes: JSON, Protocol Buffers, or Thrift. When deserialized, these serializations silently ignore extra, unexpected information. In dynamic languages, the extra information simply appears in the deserialized object.
Consider the following JSON definition for the service /user-service/v1/
:
{"userId":"UID-123","firstName":"Jake","lastName":"Cole","username":"jcole@example.com"}
The following breaking change would require re-versioning the service as /user-service/v2/
:
{"userId":"UID-123","name":"Jake Cole",# combined fields"email":"jcole@example.com"# key change}
However, the following non-breaking change doesn't require a new version:
{"userId":"UID-123","firstName":"Jake","lastName":"Cole","username":"jcole@example.com","company":"Acme Corp."# new key}
When deploying a new minor API version, App Engine allows for the new code version to be released side-by-side with the old code version. On App Engine, though you can directly address any of the deployed versions, only one version is the default serving version; recall that there is a default serving version for each service. In this example, we have our old code version, named apple
which happens to be the default serving version, and we deploy the new code version as a side-by-side version, named banana
. Note that the microservice URLs for both are the same /user-service/v1/
since we are deploying a non-breaking minor API change.
App Engine provides mechanisms to automatically migrate the traffic from apple
to banana
by marking the new code version banana
as the default serving version. When the new default serving version is set, no new requests will be routed to apple
and all new requests will be routed to banana
. This is how you roll forward to a new code version that implements a new minor or patch API version with no impact to client microservices.
In the event of an error, rolling back is achieved by reversing the above process: set the default serving version back to the old one, apple
in our example. All new requests will route back to the old code version and no new requests will route to banana
. Note that in-progress requests are allowed to complete.
App Engine also provides the ability to direct only a certain percentage of your traffic to your new code version; this process is often called a canary release process and the mechanism is called traffic splitting in App Engine. You can direct 1%, 10%, 50%, or any percentage of traffic you'd like against your new code versions, and you can adjust this amount over time. For example, you could roll out your new code version over 15 minutes, slowly increasing the traffic and watching for any issues that might identify that a rollback is required. This same mechanism lets you A/B test two code versions: set the traffic split to 50% and compare the performance and error rate characteristics of the two code versions to confirm expected improvements.
The following image shows traffic-splitting settings in the Google Cloud console:
When you deploy breaking, major API versions, the process of rolling forward and rolling back is the same as for non-breaking, minor API versions. However, you typically won't perform any traffic splitting or A/B tests because the breaking API version is a newly released URL, such as /user-service/v2/
. Of course, if you've changed the underlying implementation of your old major API version, you may still want to use traffic splitting to test that your old major API version continues to function as expected.
When deploying a new API major version, it's important to remember that old major API versions might also still be serving. For example, /user-service/v1/
might still be serving when /user-service/v2/
is released. This fact is an essential part of independent code releases. You might only turn-down old major API versions after you've verified that no other microservices require them, including other microservices that may need to roll back to an older code version.
As a concrete example, imagine that you have a microservice, named web-app
, that depends on another microservice, named user-service
. Imagine that user-service
needs to change some underlying implementation that will make it impossible to support the old major API version that web-app
is currently using, such as collapsing firstName
and lastName
into a single field called name
. That is, user-service
needs to turn-down an old major API version.
In order to accomplish this change, three separate deployments must be made:
First, user-service
must deploy /user-service/v2/
while still supporting /user-service/v1/
. This deployment may require temporary code to be written to support backwards compatibility, which is a common consequence in microservices-based applications
Next, web-app
must deploy updated code that changes its dependency from /user-service/v1/
to /user-service/v2/
Finally, after the user-service
team has verified that web-app
no longer requires /user-service/v1/
and that web-app
does not need to roll back, the team can deploy code that removes the old /user-service/v1/
endpoint and any temporary code they needed to support it.
While all this activity may seem onerous, it's an essential process in microservices-based applications and is precisely the process that enables independent development release cycles. To be clear, this process appears to be quite dependent, but importantly each step above can occur on independent timelines, and rolling forward and rollback occurs within the scope of a single microservice. Only the order of the steps is fixed and the steps could take place over many hours, days, or even weeks.
Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution 4.0 License, and code samples are licensed under the Apache 2.0 License. For details, see the Google Developers Site Policies. Java is a registered trademark of Oracle and/or its affiliates.
Last updated 2025-04-17 UTC.