Merge Endpoints

Updated:

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

also known as: Collapse Endpoints, Consolidate Operation-to-Endpoint Grouping

Context and Motivation

An API consists of multiple endpoints that expose one or more operations. Two or more of these operations are strongly related to each other. For instance, one can only be executed when the other one has succeeded, or one prepares the data that the other one works with. Because of their mutual dependencies, they have the same reasons to change and usually do so at the same time. This creates an undesired, rather tight coupling between them.

As an API provider, I want to bring the operations from multiple endpoints together so that they can be evolved jointly.

Stakeholder Concerns

#understandability (a.k.a. #explainability)
Fewer endpoints might be easier to understand and maintain — if they are cohesive and do not contain too many operations. The refactorings Move Operation and Rename Operation explain the quality attributes maintainability and understandability further.
#cohesion
See Wikipedia entry and SEBoK v. 2.4 for explanation of this general term.
#coupling
See Wikipedia entry and pattern summary of Loose Coupling.

Initial Position Sketch

The following MDSL endpoint snippet illustrates the starting position for this refactoring:

endpoint type Endpoint1
exposes 
  operation operation1 
    expecting payload "RequestMessage1" 
    delivering payload "ResponseMessage1"

endpoint type Endpoint2    
exposes
  operation operation2 
    expecting payload "RequestMessage2" 
    delivering payload "ResponseMessage2"

As the sketch above shows, the refactoring targets two endpoints and their operations.

Design Smells

Responsibility spread
The context description of the refactoring indicates that the single responsibility principle is violated. For instance, one stakeholder group might have to work with a large number of multiple endpoints to satisfy its information needs.
Extreme decomposition
The desire to decompose an API and its implementation into independently deployable units went too far. There are numerous endpoints exposing narrowly-scoped operations that call each other, or have to be orchestrated on the client side.
API does not get to the POINT
The I in POINT stands for Isolation. API operations should be free of unexpected side effects; they should not interfere with calls to other operations in the same or other APIs. See the blog post “APIs should get to the POINT” for further explanations.
Cloud-native traits violated
Cloud-Native Applications (CNAs) should have an adequate size and be modular. If two APIs depend on each other, this might not be the case. See the blog post “What is a Cloud-Native Application Anyway? 10 SUPER-IDEAL Application Properties and 7 Cloud-Native Traits” for a summary of this and other CNA traits.
Same backend system and/or domain data processed by multiple endpoints
One or more endpoints are not self-contained and autonomous, but depend on each other indirectly because they access one or more shared implementation resources (such as another system or a data store). Such “implementation spaghetti” violates several of the defining principles of service-oriented architectures and microservices, for instance independent deployability.

Instructions

  1. Move all operations from the source endpoint to the target endpoint. If an intermediate mapping/configuration from endpoints to classes/methods exists in the Web frameworks that are used to implement the API, adjust both the configuration and the code.1
  2. Delete the now orphaned/empty source endpoint from the code base.
  3. Depending on the chosen evolution strategy, implement a new stub to redirect clients to the new endpoint (in HTTP, this can be achieved with URL redirection and status code 301).
  4. Update the API test cases. Run them and compare the test results with those of the previous version to ensure that clients are served in the same way as before (both from a functional and from a non-functional point of view).
  5. Update both API Descriptions (source and target) as well as the related Service Level Agreements.
  6. Also update all API directories (registries, repositories) and/or portals (gateways, cluster managers, service meshes) that refer to source and target endpoints directly/statically.
  7. Communicate these changes to the affected clients early and consistently.

Target Solution Sketch (Evolution Outline)

endpoint type Endpoint1AndEndpoint2Merged
exposes 
  operation operation1 
    expecting payload "RequestMessage1" 
    delivering payload "ResponseMessage1"
    
   operation operation2 
    expecting payload "RequestMessage2" 
    delivering payload "ResponseMessage2"

The complete MDSL source and target sketch is here. Its OpenAPI version can be found here.

Example(s)

Reverting the target example to the initial/source in Extract Operation qualifies as an example of this refactoring. After Extract Operation has been applied, the publication management example may provide the following command and query endpoints:2

Aggregate PublicationCommandsEndpoint {
    Service PublicationManagementCommandFacade {
        // a state creation/state transition operation:
        @PaperId add(@PublicationEntryDTO newEntry);
        
        // computation operations (stateless):
        String convertToBibtex(@PublicationEntryDTO entry); 
    }
}

Aggregate PublicationQueriesEndpoint {
    Service PublicationManagementQueryFacade {
        // retrieval operations:
        @PublicationArchive dumpPublicationArchive();
        Set<@PublicationEntryDTO>lookupPublicationsFromAuthor(String writer);
        String renderAsBibtex(@PaperId paperId);
    }
}

These two endpoints can be merged into one, leading to the following API design:

Aggregate PublicationEndpoint {
    Service PublicationManagementFacade {
        // a state creation/state transition operation:
        @PaperId add(@PublicationEntryDTO newEntry);
        
        // retrieval operations:
        @PublicationArchive dumpPublicationArchive();
        Set<@PublicationEntryDTO>lookupPublicationsFromAuthor(String writer);
        String renderAsBibtex(@PaperId paperId);
        
        // computation operations (stateless):
        String convertToBibtex(@PublicationEntryDTO entry); 
    }
}

Hints and Pitfalls to Avoid

Before and when applying this refactoring, make sure to:

  • Watch out for naming conflicts; if a conflict does occur, apply the Rename Operation refactoring before the merge.
  • In HTTP resource APIs, make sure that the merge does not cause any conflicts in the operation-to-verb mappings. Each resource, uniquely identified with a URI, is able to support one GET, one POST and one PUT operation (and so forth) only. Update the URI/resource naming scheme if required, for instance via sub-resources.
  • Do not take this refactoring to the extreme; we do not advise creating one endpoint per stakeholder group.

The source and the target endpoints may have different security requirements. For instance, some operations may require rather fine-grained, attribute-based authorization while others use role-based authorization (or none). Another case might be one endpoint using basic authentication and API Keys and the other one using OpenID Connect and OAuth. Such situations can either be (viewed as) opportunities to improve the weaker security solution, or as threats because they could cause unexpected/undesired complexity. It is important to identify such mismatches before applying the refactoring and make a conscious architectural decision whether to merge or not.

This refactoring undoes Extract Operation. A simpler form of it is Move Operation. Viewed at the code level, Inline Class is a related refactoring that can be used to merge the implementation classes of the endpoints.

There are different reasons why tightly-coupled endpoints exist. If their role is data retrieval, they might provide access to the same underlying data, but for different clients. In that case, the Add Wish List refactoring might be suited.

The Service Cutter tool and method suggest sixteen coupling criteria such as “Semantic Proximity”, “Structural Volatility”, and “Security Contextuality” [Gysel et al. 2016]. These criteria are worth considering when merging endpoints and merging operations.

References

Gysel, Michael, Lukas Kölbener, Wolfgang Giersche, and Olaf Zimmermann. 2016. “Service Cutter: A Systematic Approach to Service Decomposition.” In Service-Oriented and Cloud Computing - 5th IFIP WG 2.14 European Conference, ESOCC 2016, Vienna, Austria, September 5-7, 2016, Proceedings, edited by Marco Aiello, Einar Broch Johnsen, Schahram Dustdar, and Ilche Georgievski, 9846:185–200. Lecture Notes in Computer Science. Springer. https://link.springer.com/chapter/10.1007/978-3-319-44482-6_12.

  1. An example of such framework is Play

  2. The notation in this example is CML, the tactic domain-driven design language supported by Context Mapper