Skip to content

guide service versioning

devonfw-core edited this page Dec 13, 2022 · 11 revisions

Service-Versioning

This guide describes the aspect and details about versioning of services

Motivation

Why versioning of services? First of all, you should only care about this topic if you really have to. Service versioning is complex and requires effort (time and budget). The best way to avoid this is to be smart in the first place when designing the service API. Further, if you are creating services where the only consumer is e.g. the web-client that you deploy together with the consumed services then you can change your service without the overhead to create new service versions and keeping old service versions for compatibility.

However, if the following indicators are given you typically need to do service versioning:

  • Your service is part of a complex and distributed IT landscape

  • Your service requires incompatible changes

  • There are many consumers or there is at least one (relevant) consumer that can not be updated at the same time or is entirely out of control (unknown or totally different party/company)

What are incompatible changes?

  • Almost any change when SOAP is used (as it changes the WSDL and breaks the contract). Therefore, we recommend to use REST instead. Then, only the following changes are critical.

  • A change where existing properties (attributes) have to change their name

  • A change where existing features (properties, operations, etc.) have to change their semantics (meaning)

What changes do not cause incompatibilities?

  • Adding new service operations is entirely uncritical with REST.

  • Adding new properties is only a problem in the following cases:

    • Adding new mandatory properties to the input of a service is causing incompatibilities. This problem can be avoided by contract-design.

    • If a consumer is using a service to read data, modify it and then save it back via a service and a property is added to the data, then this property might be lost. This is not a problem with dynamic languages such as JavaScript/TypeScript but with strictly typed languages such as Java. In Java you will typically use structured typed transfer-objects (and not Map<String, Object>) so new properties that have been added but are not known to the consumer can not be mapped to the transfer-object and will be lost. When saving that transfer-object later the property will be gone. It might be impossible to determine the difference between a lost property and a property that was removed on purpose. This is a general problem that you need to be aware of and that you have to consider by your design in such situations.

Even if you hit an indicator for incompatible changes you can still think about adding a new service operation instead of changing an existing one (and deprecating the old one). Be creative to simplify and avoid extra effort.

Procedure

The procedure when rolling out incompatible changes is illustrated by the following example:

+------+  +------+
| App1 |  | App2 |
+---+--+  +--+---+
    |        |
    +---+----+
        |
+-------+--------+
|      Sv1       |
|                |
|      App3      |
+----------------+

So, here we see a simple example where App3 provides a Service S in Version v1 that is consumed both by App1 and App2.

Now for some reason the service S has to be changed in an incompatible way to make it future-proof for demands. However, upgrading all 3 applications at the same time is not possible in this case for whatever reason. Therefore, service versioning is applied for the changes of S.

+------+  +------+
| App1 |  | App2 |
+---+--+  +--+---+
    |        |
    +--------+
    |
+---+------------+
|  Sv1  |  Sv2   |
|                |
|      App3*     |
+----------------+

Now, App3 has been upgraded and the new release was deployed. A new version v2 of S has been added while v1 is still kept for compatibility reasons and that version is still used by App1 and App2.

+------+  +------+
| App1 |  | App2*|
+---+--+  +--+---+
    |        |
    |        |
    |        |
+---+--------+---+
|  Sv1  |  Sv2   |
|                |
|      App3      |
+----------------+

Now, App2 has been updated and deployed and it is using the new version v2 of S.

+------+  +------+
| App1*|  | App2 |
+---+--+  +--+---+
    |        |
    +--------+
             |
+------------+---+
|  Sv1  |  Sv2   |
|                |
|      App3      |
+----------------+

Now, also App1 has been updated and deployed and it is using the new version v2 of S. The version v1 of S is not used anymore. This can be verified via logging and monitoring.

+------+  +------+
| App1 |  | App2 |
+---+--+  +--+---+
    |        |
    +--------+
             |
+------------+---+
|          Sv2   |
|                |
|      App3*     |
+----------------+

Finally, version v1 of the service S was removed from App3 and the new release has been deployed.

Versioning Schema

In general anything can be used to differentiate versions of a service. Possibilities are:

  • Code names (e.g. Strawberry, Blueberry, Grapefruit)

  • Timestamps (YYYYMMDD-HHmmSS)

  • Sequential version numbers (e.g. v1, v2, v3)

  • Composed version numbers (e.g. 1.0.48-pre-alpha-3-20171231-235959-Strawberry)

As we are following the KISS principle (see key principles) we propose to use sequential version numbers. These are short, clear, and easy while still allowing to see what version is after another one. Especially composed version numbers (even 1.1 vs. 2.0) lead to decisions and discussions that easily waste more time than adding value. It is still very easy to maintain an Excel sheet or release-notes document that is explaining the changes for each version (v1, v2, v3) of a particular service.

We suggest to always add the version schema to the service URL to be prepared for service versioning even if service versioning is not (yet) actively used. For simplicity it is explicitly stated that you may even do incompatible changes to the current version (typically v1) of your service if you can update the according consumers within the same deployment.

Practice

So assuming you know that you have to do service versioning, the question is how to do it practically in the code. The approach for your devon4j project in case of code-first should be as described below:

  • Determine which types in the code need to be changed. It is likely to be the API and implementation of the according service but it may also impact transfer objects and potentially even datatypes.

  • Create new packages for all these concerned types containing the current version number (e.g. v1).

  • Copy all these types to that new packages.

  • Rename these copies so they carry the version number as suffix (e.g. V1).

  • Increase the version of the service in the unversioned package (e.g. from v1 to v2).

  • Now you have two versions of the same service (e.g. v1 and v2) but so far they behave exactly the same.

  • You start with your actual changes and modify the original files that have been copied before.

  • You will also ensure the links (import statements) of the copied types point to the copies with the version number

  • This will cause incompatibilities (and compile errors) in the copied service. Therefore, you need to fix that service implementation to map from the old API to the new API and behavior. In some cases, this may be easy (e.g. mapping x.y.z.v1.FooTo to x.y.z.FooTo using bean-mapping with some custom mapping for the incompatible changes), in other cases this can get very complex. Be aware of this complexity from the start before you make your decision about service versioning.

  • As far as possible this mapping should be done in the service-layer, not to pollute your business code in the core-layer with versioning-aspects. If there is no way to handle it in the service layer, e.g. you need some data from the persistence-layer, implement the "mapping" in the core-layer then, but don’t forget to remove this code, when removing the old service version.

  • Finally, ensure that both the old service behaves as before as well as the new service works as planned.

Modularization

For modularization, we also follow the KISS principle (see key principles): we suggest to have one api module per application that will contain the most recent version of your service and get released with every release-version of the application. The compatibility code with the versioned packages will be added to the core module and therefore is not exposed via the api module (because it has already been exposed in the previous release of the app). This way, you can always determine for sure which version of a service is used by another application just by its maven dependencies.

The KISS approach with only a single module that may contain multiple services (e.g. one for each business component) will cause problems when you want to have mixed usages of service versions: You can not use an old version of one service and a new version of another service from the same APP as then you would need to have its API module twice as a dependency on different versions, which is not possible. However, to avoid complicated overhead we always suggest to follow this easy approach. Only if you come to the point that you really need this complexity you can still solve it (even afterwards by publishing another maven artefact). As we are all on our way to build more but smaller applications (SOA, microservices, etc.) we should always start simple and only add complexity when really needed.

The following example gives an idea of the structure:

/«my-app»
├──/api
|  └──/src/main/java/
|     └──/«rootpackage»/«application»/«component»
|        ├──/common/api/to
|        |  └──FooTo
|        └──/service/api/rest
|           └──FooRestService
└──/core
   └──/src/main/java/
      └──«rootpackage»/«application»/«component»
         ├──/common/api/to/v1
         |  └──FooToV1
         └──/service
            ├──/api/rest/v1
            |  └──FooRestServiceV1
            └──impl/rest
               ├──/v1
               |  └── FooRestServiceImplV1
               └──FooRestServiceImpl
Clone this wiki locally