Introduce Version Mediator

Updated: Published: EuroPLoP 2023

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 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 quickly to changes introduced by their public 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” [Lübke et al. 2019] for a general discussion of desired qualities, their conflicts, and related trade-offs.

Initial Position Sketch

This architectural 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

See Figure 1 for a visualization of the initial position.

Introduce Version Mediator: Initial Position Sketch. Client communicates with Version 1 of an API Endpoint

Figure 1: Introduce Version Mediator: Initial Position Sketch. Client communicates with Version 1 of an API Endpoint

Design Smells

Evolution strategy does not meet 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
One or more breaking change of the API have happened, or the lifetime guarantee has been softened.2 However, clients are unwilling or unable to migrate to the latest version immediately. They might fear the effort and risk of the migration or they might lack confidence and trust in the new version.
Large and/or partially unknown user base
API providers are not in control of their users and lack 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. Define a default value or 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 makes 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 Splitter 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 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 a single element to return. However, the “old” client and the mediator might suffer from information loss through this filtering. Additional patterns such as Context Representation might be able to provide additional (meta-)information in such situations.3

Include any custom request and response headers in the mapping for requests and responses.

Secure the mediator endpoint exactly as the updated main endpoint.

Target Solution Sketch (Evolution Outline)

A rule-based Compatibility Mediator implements the compatibility mappings, either as plain code or declaratively, and acts as a gateway between “old” clients and the “new” provider. Figure 2 shows this solution.

Introduce Version Mediator: Target Solution Sketch. Client communication is mapped to a new endpoint via a Compatibility Mediator.

Figure 2: Introduce Version Mediator: Target Solution Sketch. Client communication is mapped to a new endpoint via a Compatibility Mediator.

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 the following Customer Core microservice (notation: MDSL):

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 all deviations here (as defined 
// in Steps 1 to 6 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
  // will no longer work:
  consumes CustomerCoreOriginalContract
  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

Hints and Pitfalls to Avoid

The user story and the smells motivating this refactoring mention scenarios in which it may be applied; 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.

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.

Having decided to apply this refactoring, make sure to:

  • 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 introduced Content Filters realizing the Version Mediator are not prepared to process).
  • Monitor the performance of the Compatibility Mediator (in particular, when it is realized as a mapping rule engine) and end-to-end latency (as 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 a 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”. LinkedIn also uses an API Gateway in their new Marketing APIs that support request mapping to mediate between API versions.

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 Enterprise Integration Patterns category Message Transformation provides partial solutions; the patterns Content Enricher and Content Filter are used to realize the Compatibility Mediator (which effectively is a special-purpose Content-Based Router).

For an example of Enterprise Service Bus 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 letting 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.