Introduce Version Mediator

Updated:

 Note: This is a preview release and subject to change. Feedback welcome! Contact Information and Background (PDF)

also known as: Add Compatibility Gateway, Add Tolerant Proxy, Support Virtual Version

Context and Motivation

An API runs in production. One of its supported versions will be retired soon, but existing clients still use it. One or more breaking changes have been introduced in subsequent, active versions.1

As an API client, I want to continue to call a deprecated or sunset API for some time, and I expect the provider to support me with a temporary solution for doing so. The behavior of this solution should be identical to those of the API that I have been using so far.

We will call the API version that will go out of service the “old” API, and its successor “new” API.

Stakeholder Concerns (including Quality Attributes and Design Forces)

#flexibility and #evolvability
To evolve an API, providers must have the flexibility to refactor, redesign, and adapt an API over time. Ideally, this happens without breaking compatibility, but this is not always realistic.
#developer-experience
Breaking changes in APIs and pressure to upgrade may frustrate client developers, especially if they must migrate to a newer version on short notice. For instance, cloud application developers sometimes have to react rather quick to infrastructure changes introduced by their public cloud providers.
#maintainability
Fewer components and/or code paths that handle deprecated behavior make an API and its implementation easier to maintain for the provider.

See “Interface Evolution Patterns — Balancing Compatibility and Extensibility across Service Life Cycles” by Daniel Lübke et al. [Lübke et al. 2019] for a general discussion of desired qualities, their conflicts, and related trade-offs.

Initial Position Sketch

This refactoring affects the following API elements:

  • An endpoint and at least one of its operations (with their roles and responsibilities)
  • Representation elements in request and response messages of these operations with their names, roles, and types (including information about value ranges, optionality, and cardinality)
  • API clients and the remote communication proxies they use

Smells / Drivers

Evolution strategy does not match client expectations
The API provider has decided to commit a short lifetime of an API version only, or has announced to retire one or more active versions soon. This has caused a negative reaction in the API client community (the related refactorings Tighten Evolution Strategy and Relax Evolution Strategy explain this smell further).
Resistance to change caused by uncertainty
A breaking change of the API has happened or the lifetime guarantee been softened.2 However, clients are unwilling or unable to migrate to the latest version immediately. They might fear the effort and risk, or lack confidence and trust in the new version.
Large and/or partially unknown user base
The API provider is not in full control of its users and lacks information about them. The less information and control a provider has, the higher the risk of impacting clients negatively (or losing them) when making breaking changes in upgrades.

Instructions (Steps)

Add a new endpoint to the API for the “new” API version. Derive its contract from the “old” one and adjust the “old” endpoint to mediate “old” operations and their representation elements to “new” ones with mapping rules.

When establishing the mapping rules for the operations whose “old” and “new” versions are incompatible, start with the request messages:

  1. Identify and mark the fields that remain unchanged and do not require a mapping.
  2. Define a mapping rule for fields that are renamed only and can be mapped one-to-one (pass-through).
  3. Find the fields that change their type, and define a mapping rule to implement the type change:
    • If a previously optional field becomes mandatory, define a mapping rule that leverages a Content Enricher a.k.a. Data Enricher to define a default value (filler). No mapping action is required for the opposite case (mandatory fields becoming optional).
    • Mark the fields that change cardinality, for instance from atomic to list/set (and vice versa).
    • Do the same for other basic type changes, such as from numeric to strings.
  4. Find the fields that are added in the new version. Add a Content Enricher to support “old” clients and/or define a default value and make the newcomer optional (to preserve compatibility).
  5. Mark the fields that disappear in the new version. Add a Content Filter to mediate requests from “old” clients (and log the fact that some data is no longer processed, having double checked that this make sense).
  6. Find places where two (or more) “old” fields map to one “new” field. For each such place, define a mapping rule realizing an Aggregation strategy. Define a Split or sequencer rule for the opposite case, one “old” field mapping to two or more “new” fields. These two cases can also occur in combination (which actually can be seen as the default/catch case); a Scatter-Gather rule (or broadcast aggregate) can handle them.

Continue with the response messages and define similar rules for them, including error cases:

  • If a representation element in a response used to be a single Atomic Parameter but now is set- or list-valued, a Content Filter can select list elements. However, the “old” client and the mediator might suffer from information loss. Additional patterns such as Context Representation and Linked Information Holder that place the new information elsewhere might be able to help in such situations.3

Both for requests and responses, include any custom request headers and response headers in the mapping.

Secure the mediator endpoint exactly as the updated main endpoint.

Target Solution Sketch (Evolution Outline)

A rule-base mediator implements the compatibility mappings, either as plain code or declaratively, acts as a gateway between “old” clients and the “new” provider:

The mediator should be a transitional, interim solution preserving a good client developer experience and giving clients more time to adopt the “new” API.

Example(s)

The fictitious insurance firm Lakeside Mutual could expose this customer core microservice:

API description LakesideMutual version "v1.0.0"

data type CustomerDTO1 {"name":D<string>}

endpoint type CustomerCoreOriginalContract
exposes 
  operation createCustomerMasterData
    expecting payload "customerData": CustomerDTO1
    delivering payload "customerId": ID<int>
  operation readCustomerMasterData 
    expecting payload "customerId": ID<int>
    delivering payload "customerData": CustomerDTO1

API provider LakesideMutualAPI
  offers CustomerCoreOriginalContract
  at endpoint location "http://some.original.address"
  via protocol HTTP binding resource Home

API client CustomerRelationshipManagementApplication
  consumes CustomerCoreOriginalContract
  via protocol HTTP 

Lakeside Mutual could then update its customer core interface (for instance, after a merger with another company):

API description LakesideMutual version "v2.0.0"

data type CustomerDTO1 {"name":D<string>, "zipString":D<string>, "toBeSunset":MD<bool>}

data type CustomerDTO2 {"firstName":D<string>, "lastName":D<string>, 
    "zipCode": D<int>, "newKey":ID<int>} 
// not featuring other deviations here (see rules in Steps in "Instructions") 
   
endpoint type CustomerCoreRevisedContract
exposes 
  operation createCustomerMasterData
    expecting payload "customerData": CustomerDTO2
    delivering payload "customerId": ID<int>
  operation readCustomerMasterData 
    expecting payload "customerId": ID<int>
    delivering payload "customerData": CustomerDTO2
   
API client CustomerRelationshipManagementApplication
  consumes CustomerCoreOriginalContract // will no longer work
  via protocol HTTP 

Support for the “old” contract can be modeled as a mediation gateway in MDSL (which does not imply that a visible API gateway is deployed; the mediation can happen in the background):

API gateway Version1ToVersion2Mediator
  offers CustomerCoreOriginalContract
    at endpoint location "http://some.new.address"
    via protocol HTTP binding resource Home
  consumes CustomerCoreRevisedContract 
    from LakesideMutualAPI
    via protocol HTTP
  mediates from CustomerDTO1 to CustomerDTO2 
    element zipString to zipCode
    // not featuring element-level mapping rules here yet

Hints and Pitfalls to Avoid

The user story and the smells at the top motivate scenarios in which this refactoring may be applied; but it is also important to know when not to use a particular design.

To decide when not to apply this refactoring, analyze whether the roles of an endpoint, responsibilities of an operation, and/or semantics of a data element change. Such changes require more than a rule-based mediator; in such situations, this refactoring is less suited.

Having decided to apply this refactoring, make sure to:

  • As a general rule for any communication party (API client and provider), apply Postel’s Law and be liberal when consuming messages and conservative when producing them.
  • Catch and handle mapping errors, both at specification time and at runtime.
  • Test all combinations of the “old” versus “new” clients and provider that appear in the refactored system landscape; up to four (two times two) cases might occur. Add test data for all steps/situations from the step descriptions further up that may occur. Include mapping errors in the tests (for instance, set-valued responses that the Content Filters are not prepared to process).
  • Monitor the performance of the mapping rule engine (if any) and end-to-end latency (a call chain is introduced and the number of request/response messages is doubled).
  • Do not prolong the lifetime of the intermediary/the temporary mediation endpoint; for instance, do not place a second gateway in front of the gateway to cope with a future change of a different kind.

A domain-specific language, either embedded in a general-purpose language or explicit, might be an appropriate choice for expressing the mapping rules. Many application integration tools provide such languages (often proprietary). MDSL is an emerging, technology-independent contract language that supports mediation rules.

An API Gateway may play the role of a Version Mediator. An example of such gateway usage can be found in the blog post “8 Common API Gateway Request Transformation Policies”.

An application of Tighten Evolution Strategy may trigger this refactoring. And Introduce Version Identifier might have to be applied before this one so that clients can learn about versions and their (in-)compatibilities.

The EIP category Message Transformation provides partial solutions; the patterns Content Enricher and Content Filter are particularly relevant here (so is Content-Based Router).

For an example ESB product capabilities and integration services, refer to Scenario 4/Figure 7 in “Enterprise Service Bus” by Jürgen Kress et al. This technical article, available on a vendor site, positions the pattern presents usage scenarios, and suggests selection criteria.

In object-oriented programming, the Adapter design pattern [Gamma et al. 1995] provides a different view on the interface of a class so that it can be used by clients that cannot work with the original interface easily. Note that there is also a Mediator pattern, whose goal is different (decouple objects from each other).

Also related are:

Smart proxies in service middleware operating on change-aware contracts are emerging [Knoche and Hasselbring 2021].

References

Gamma, Erich, Richard Helm, Ralph Johnson, and John Vlissides. 1995. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.

Knoche, Holger, and Wilhelm Hasselbring. 2021. “Continuous API Evolution in Heterogenous Enterprise Software Systems.” In 18th IEEE International Conference on Software Architecture, ICSA 2021, Stuttgart, Germany, March 22-26, 2021, 58–68. IEEE. https://doi.org/10.1109/ICSA51549.2021.00014.

Lübke, Daniel, Olaf Zimmermann, Cesare Pautasso, Uwe Zdun, and Mirko Stocker. 2019. “Interface Evolution Patterns: Balancing Compatibility and Extensibility Across Service Life Cycles.” In Proceedings of the 24th European Conference on Pattern Languages of Programs. EuroPLop ’19. New York, NY, USA: Association for Computing Machinery. https://doi.org/10.1145/3361149.3361164.

  1. which should, but cannot always be avoided; sometimes, it is better to break an existing client and cause work for its developers/maintainers rather than pretending that nothing has changed and let some hard-to-catch bugs creep in 

  2. while this should generally be avoided, this is not always possible; the provider might have good reasons to do it 

  3. such cases should be avoided if at all possible, for instance, by providing the “old” and the “new” version of the operation in parallel in the “new” API